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 Copyright (C) 2026 Guillaume Stutin.
5
6 Ansel is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
10
11 Ansel is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with Ansel. If not, see <http://www.gnu.org/licenses/>.
18*/
19
21#include "caches/image_cache.h"
22#include "caches/mipmap_cache.h"
23#include "common/opencl.h"
25#include "common/conf.h"
26#include "develop/supervisor.h"
27#include "gui/application.h"
29#include "widgets/scroll_wrap.h"
31
32#include <gtk/gtk.h>
33#include <json-glib/json-glib.h>
34#ifdef GDK_WINDOWING_WAYLAND
35#include <gdk/gdkwayland.h> // conditional-ok: GDK_IS_WAYLAND_DISPLAY() is used only inside the same #ifdef
36#endif
37
38// Cap on how many timeline rows we keep live (older ones are trimmed). The full
39// log lives in the supervisor; this only bounds GTK widget pressure.
40#define TIMELINE_MAX_ROWS 4000
41// How far past the cap the list may grow before it is trimmed back to it. Removing the top
42// rows shifts every other one, and GTK 3 then restyles them all: about 0.45 s at 4000 rows on a
43// mock with the theme, however few go. Trimmed at every poll, 24 polls of 50 events took 14.8 s;
44// trimmed past this slack, 5.8 s, a 1.8 s trim once every 1000 events.
45#define TIMELINE_TRIM_SLACK (TIMELINE_MAX_ROWS / 4)
46#define POLL_INTERVAL_MS 300
47#define MEMORY_MAX_ROWS 500 // cap items shown per cache in the memory view
48#define MEMORY_REFRESH_TICKS 4 // refresh the memory view every Nth poll tick when visible
49#define MEMORY_LIST_ROWS 20 // how many rows a memory list shows until the user resizes it
50
51static struct
52{
58 GtkWidget *mem_box; // vbox holding the memory view content
59 GHashTable *mem_lists; // memory section key -> its list's resizable area (a reference), kept across rebuilds
60 GtkWidget *search_entry; // global search entry (in the toolbar)
61 GtkWidget *search_list; // search results page
63 GtkWidget *groupby; // GtkComboBoxText: domain / thread / op
64 GHashTable *decl_map; // hex string -> GtkListBoxRow* (the create/declaration row)
65 GHashTable *group_map; // group key -> _group_t* (incremental grouped buckets)
66 uint64_t last_seq; // highest captured seq already displayed in the timeline
67 uint64_t grouped_last_seq; // highest seq already folded into the grouped view
68 gchar *scroll_target; // hash of a timeline row to scroll into view (deferred)
69 guint timer_id;
70 guint tick; // poll tick counter (for throttling the memory view)
71 int page_timeline, page_grouped, page_memory, page_search; // notebook page indices
72} _g = { 0 };
73
74// One grouped bucket: its detail body (where event rows are appended) and the
75// header label (whose count we keep in sync).
83
84static void _group_free(gpointer p)
85{
86 _group_t *g = (_group_t *)p;
87 if(!g) return;
88 g_free(g->key);
89 g_free(g); // widgets are owned by the list box
90}
91
92static gchar *_hashhex(const uint64_t h)
93{
94 return g_strdup_printf("0x%016" G_GINT64_MODIFIER "x", h);
95}
96
97static const char *_domain_color(const char *d)
98{
99 if(!g_strcmp0(d, "history")) return "#7fbfff";
100 if(!g_strcmp0(d, "node")) return "#9fe0a0";
101 if(!g_strcmp0(d, "cacheline")) return "#ffd080";
102 if(!g_strcmp0(d, "backbuf")) return "#ff9a9a";
103 if(!g_strcmp0(d, "widget")) return "#d0a8ff";
104 if(!g_strcmp0(d, "thumbnail")) return "#bcbcbc";
105 if(!g_strcmp0(d, "mipmap")) return "#b0c4de";
106 if(!g_strcmp0(d, "image")) return "#e8c8a0";
107 if(!g_strcmp0(d, "form")) return "#f0a0c0";
108 return "#dddddd";
109}
110
111static gchar *_pretty_json(const char *compact)
112{
113 JsonParser *p = json_parser_new();
114 gchar *out = NULL;
115 if(json_parser_load_from_data(p, compact, -1, NULL))
116 {
117 JsonGenerator *g = json_generator_new();
118 json_generator_set_root(g, json_parser_get_root(p));
119 json_generator_set_pretty(g, TRUE);
120 out = json_generator_to_data(g, NULL);
121 g_object_unref(g);
122 }
123 g_object_unref(p);
124 return out ? out : g_strdup(compact);
125}
126
127static void _run_search(const char *query);
128
129// Scroll a timeline row (centred) into the visible area of its scrolled window.
131{
132 if(!row || !_g.timeline_scroll) return;
133 GtkAdjustment *vadj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(_g.timeline_scroll));
134 if(!vadj) return;
135 gint y = 0;
136 if(!gtk_widget_translate_coordinates(row, _g.timeline_list, 0, 0, NULL, &y)) return;
137 GtkAllocation alloc;
138 gtk_widget_get_allocation(row, &alloc);
139 const double page = gtk_adjustment_get_page_size(vadj);
140 const double upper = gtk_adjustment_get_upper(vadj);
141 double target = (double)y + alloc.height / 2.0 - page / 2.0; // centre the row
142 target = CLAMP(target, 0.0, MAX(0.0, upper - page));
143 gtk_adjustment_set_value(vadj, target);
144}
145
146// Deferred so the scroll runs after the page switch / row expansion are laid out.
147static gboolean _scroll_target_idle(gpointer u)
148{
149 if(_g.window && _g.scroll_target)
150 {
151 GtkWidget *row = (GtkWidget *)g_hash_table_lookup(_g.decl_map, _g.scroll_target);
153 }
154 g_clear_pointer(&_g.scroll_target, g_free);
155 return G_SOURCE_REMOVE;
156}
157
158// Clicking a hash jumps to the declaration (create event) of that object.
159static gboolean _on_link(GtkLabel *label, gchar *uri, gpointer user_data)
160{
161 if(!_g.decl_map) return TRUE;
162 GtkWidget *row = (GtkWidget *)g_hash_table_lookup(_g.decl_map, uri);
163 if(row)
164 {
165 gtk_notebook_set_current_page(GTK_NOTEBOOK(_g.notebook), _g.page_timeline);
166 GtkWidget *toggle = (GtkWidget *)g_object_get_data(G_OBJECT(row), "toggle");
167 if(toggle) gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(toggle), TRUE); // expand
168 gtk_list_box_select_row(GTK_LIST_BOX(_g.timeline_list), GTK_LIST_BOX_ROW(row));
169 gtk_widget_grab_focus(row);
170 // Defer the explicit scroll: the page just switched and the row may not be
171 // laid out yet, so translate_coordinates would be stale right now.
172 g_free(_g.scroll_target);
173 _g.scroll_target = g_strdup(uri);
174 g_idle_add(_scroll_target_idle, NULL);
175 }
176 return TRUE; // handled: do not try to open as an URL
177}
178
179static void _on_search_this_hash(GtkMenuItem *item, gpointer u)
180{
181 const char *uri = (const char *)g_object_get_data(G_OBJECT(item), "uri");
182 if(uri && _g.search_entry)
183 {
184 gtk_entry_set_text(GTK_ENTRY(_g.search_entry), uri); // triggers search-changed → _run_search
185 gtk_notebook_set_current_page(GTK_NOTEBOOK(_g.notebook), _g.page_search);
186 }
187}
188
189// Right-click over a hash link offers to search for that hash.
190static gboolean _on_label_button(GtkWidget *label, GdkEventButton *e, gpointer u)
191{
192 if(e->type != GDK_BUTTON_PRESS || e->button != GDK_BUTTON_SECONDARY) return FALSE;
193 const char *uri = gtk_label_get_current_uri(GTK_LABEL(label)); // link under the pointer
194 // Inside a list box (timeline/grouped/search) the pointer-tracking that sets
195 // the "current link" is unreliable, so fall back to the label's single own hash.
196 if(!uri || !*uri) uri = (const char *)g_object_get_data(G_OBJECT(label), "own-hash");
197 if(!uri || !*uri) return FALSE;
198
199 GtkWidget *menu = gtk_menu_new();
200 GtkWidget *mi = gtk_menu_item_new_with_label(_("Search for this hash"));
201 g_object_set_data_full(G_OBJECT(mi), "uri", g_strdup(uri), g_free);
202 g_signal_connect(mi, "activate", G_CALLBACK(_on_search_this_hash), NULL);
203 gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi);
204 gtk_widget_show_all(menu);
205 gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent *)e);
206 return TRUE;
207}
208
209// Wire a hash-bearing label: left-click navigates, right-click offers search.
210static void _wire_hash_label(GtkWidget *label)
211{
212 gtk_label_set_track_visited_links(GTK_LABEL(label), FALSE);
213 g_signal_connect(label, "activate-link", G_CALLBACK(_on_link), NULL);
214 gtk_widget_add_events(label, GDK_BUTTON_PRESS_MASK);
215 g_signal_connect(label, "button-press-event", G_CALLBACK(_on_label_button), NULL);
216}
217
218static GtkWidget *_event_body_from(const char *json, GArray *links);
219
220static void _on_toggle(GtkToggleButton *t, gpointer u)
221{
222 const gboolean a = gtk_toggle_button_get_active(t);
223 GtkWidget *rev = (GtkWidget *)g_object_get_data(G_OBJECT(t), "revealer");
224
225 // Lazily build an event's detail body the first time it is expanded, so the
226 // JSON is not parsed for rows the user never opens (important under streaming).
227 if(a && rev && !gtk_bin_get_child(GTK_BIN(rev)))
228 {
229 const char *json = (const char *)g_object_get_data(G_OBJECT(t), "lazy_json");
230 if(json)
231 {
232 GtkWidget *body = _event_body_from(json, (GArray *)g_object_get_data(G_OBJECT(t), "lazy_links"));
233 gtk_container_add(GTK_CONTAINER(rev), body);
234 gtk_widget_show_all(body);
235 }
236 }
237 if(rev) gtk_revealer_set_reveal_child(GTK_REVEALER(rev), a);
238 gtk_button_set_label(GTK_BUTTON(t), a ? "▾" : "▸");
239}
240
241// A collapsible block: [arrow toggle][header label] + a revealer holding `body`.
242// The header is a separate label (NOT the toggle), so its hash links stay
243// clickable. Returns a vbox; the toggle button is stored as data "toggle".
244// The arrow is the button's text, not an icon: on Windows, every GtkImage drawn keeps a DC and a
245// DIB section, and with one arrow per event row that ran Ansel out of GDI objects (see
246// doc/gtk-patterns.md). A label costs none.
247static GtkWidget *_collapsible(const char *header_markup, GtkWidget *body, const gboolean expanded)
248{
249 GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
250 GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
251
252 GtkWidget *toggle = gtk_toggle_button_new_with_label(expanded ? "▾" : "▸");
253 gtk_button_set_relief(GTK_BUTTON(toggle), GTK_RELIEF_NONE);
254 gtk_widget_set_focus_on_click(toggle, FALSE);
255
256 GtkWidget *header = gtk_label_new(NULL);
257 gtk_label_set_markup(GTK_LABEL(header), header_markup);
258 gtk_label_set_xalign(GTK_LABEL(header), 0.0);
259 _wire_hash_label(header);
260
261 GtkWidget *revealer = gtk_revealer_new();
262 gtk_revealer_set_reveal_child(GTK_REVEALER(revealer), expanded);
263 if(body) gtk_container_add(GTK_CONTAINER(revealer), body);
264
265 g_object_set_data(G_OBJECT(toggle), "revealer", revealer);
266 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(toggle), expanded);
267 g_signal_connect(toggle, "toggled", G_CALLBACK(_on_toggle), NULL);
268
269 gtk_box_pack_start(GTK_BOX(hbox), toggle, FALSE, FALSE, 0);
270 gtk_box_pack_start(GTK_BOX(hbox), header, TRUE, TRUE, 0);
271 gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);
272 gtk_box_pack_start(GTK_BOX(vbox), revealer, FALSE, FALSE, 0);
273
274 g_object_set_data(G_OBJECT(vbox), "toggle", toggle);
275 g_object_set_data(G_OBJECT(vbox), "header", header); // so callers can update the label
276 return vbox;
277}
278
279static gchar *_header_markup(const dt_sv_logged_event_t *ev)
280{
281 gchar *hx = _hashhex(ev->hash);
282 gchar *e_op = g_markup_escape_text(ev->op, -1);
283 gchar *e_dom = g_markup_escape_text(ev->domain, -1);
284 gchar *e_thr = g_markup_escape_text(ev->thread, -1);
285 gchar *e_mn_raw = g_markup_escape_text(ev->mnemonic[0] ? ev->mnemonic : "-", -1);
286 // Raster masks are side-band cachelines, not module outputs: dim the domain
287 // badge and tag the title so they read as secondary entries among plain
288 // image cachelines of the same module.
289 const gboolean is_raster_mask
290 = !g_strcmp0(ev->domain, "cacheline") && !g_strcmp0(ev->cache_name, "raster mask");
291 gchar *e_mn = is_raster_mask ? g_strdup_printf("%s - raster mask", e_mn_raw) : g_strdup(e_mn_raw);
292 g_free(e_mn_raw);
293 gchar *out = g_strdup_printf(
294 "<tt>%9.3f</tt> <b>%-7s</b> <span foreground=\"%s\">%-10s</span> <b>%-16s</b> "
295 "<a href=\"%s\"><tt>%s</tt></a> <span size=\"small\"><i>%s</i></span>",
296 ev->ts, e_op, is_raster_mask ? "#b78d45" : _domain_color(ev->domain), e_dom, e_mn, hx, hx, e_thr);
297 g_free(hx);
298 g_free(e_op);
299 g_free(e_dom);
300 g_free(e_thr);
301 g_free(e_mn);
302 return out;
303}
304
305// Build the detail body for an event: clickable linked hashes + full record.
306static GtkWidget *_event_body_from(const char *json, GArray *links)
307{
308 GtkWidget *detail = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
309 gtk_widget_set_margin_start(detail, 28);
310 gtk_widget_set_margin_bottom(detail, 6);
311
312 if(links && links->len)
313 {
314 GString *ls = g_string_new(NULL);
315 for(guint i = 0; i < links->len; i++)
316 {
317 const dt_sv_link_t *lk = &g_array_index(links, dt_sv_link_t, i);
318 gchar *lhx = _hashhex(lk->hash);
319 g_string_append_printf(ls, "%s%s → <a href=\"%s\"><tt>%s</tt></a>", i ? " " : "", lk->label,
320 lhx, lhx);
321 g_free(lhx);
322 }
323 GtkWidget *lw = gtk_label_new(NULL);
324 gtk_label_set_markup(GTK_LABEL(lw), ls->str);
325 gtk_label_set_xalign(GTK_LABEL(lw), 0.0);
327 gtk_box_pack_start(GTK_BOX(detail), lw, FALSE, FALSE, 0);
328 g_string_free(ls, TRUE);
329 }
330
331 gchar *pretty = _pretty_json(json);
332 gchar *escaped = g_markup_escape_text(pretty, -1);
333 gchar *mono = g_strdup_printf("<tt>%s</tt>", escaped);
334 GtkWidget *body = gtk_label_new(NULL);
335 gtk_label_set_markup(GTK_LABEL(body), mono);
336 gtk_label_set_xalign(GTK_LABEL(body), 0.0);
337 gtk_label_set_selectable(GTK_LABEL(body), TRUE);
338 gtk_box_pack_start(GTK_BOX(detail), body, FALSE, FALSE, 0);
339 g_free(pretty);
340 g_free(escaped);
341 g_free(mono);
342 return detail;
343}
344
345static void _links_free(gpointer p)
346{
347 if(p) g_array_free((GArray *)p, TRUE);
348}
349
350// Collapsed event row; the detail body is built lazily on first expand.
352{
353 gchar *hm = _header_markup(ev);
354 GtkWidget *w = _collapsible(hm, NULL, FALSE);
355 g_free(hm);
356
357 GtkWidget *toggle = (GtkWidget *)g_object_get_data(G_OBJECT(w), "toggle");
358 g_object_set_data_full(G_OBJECT(toggle), "lazy_json", g_strdup(ev->json), g_free);
359 if(ev->links && ev->links->len)
360 {
361 GArray *copy = g_array_new(FALSE, FALSE, sizeof(dt_sv_link_t));
362 g_array_append_vals(copy, ev->links->data, ev->links->len);
363 g_object_set_data_full(G_OBJECT(toggle), "lazy_links", copy, _links_free);
364 }
365
366 // The header carries exactly one hash (the event's own): stash it so right-click
367 // search works even when the list box defeats the label's link tracking.
368 GtkWidget *header = (GtkWidget *)g_object_get_data(G_OBJECT(w), "header");
369 if(header) g_object_set_data_full(G_OBJECT(header), "own-hash", _hashhex(ev->hash), g_free);
370 return w;
371}
372
373// Append one event to the timeline list and register it as a declaration when
374// it is a create event.
376{
377 GtkWidget *w = _event_widget(ev);
378 gtk_widget_show_all(w);
379 gtk_container_add(GTK_CONTAINER(_g.timeline_list), w);
380 GtkWidget *row = gtk_widget_get_parent(w); // the auto-created GtkListBoxRow
381 g_object_set_data(G_OBJECT(row), "toggle", g_object_get_data(G_OBJECT(w), "toggle"));
382 if(!g_strcmp0(ev->op, "create"))
383 {
384 gchar *hx = _hashhex(ev->hash);
385 g_object_set_data_full(G_OBJECT(row), "svhash", g_strdup(hx), g_free);
386 g_hash_table_replace(_g.decl_map, hx, row); // map owns hx
387 }
388}
389
390// Make room for `incoming` rows: once they would take the list past the cap and its slack, drop
391// the oldest rows down to the cap.
392static void _timeline_trim(const guint incoming)
393{
394 GList *children = gtk_container_get_children(GTK_CONTAINER(_g.timeline_list));
395 const int rows = (int)g_list_length(children) + (int)incoming;
396 int excess = rows > TIMELINE_MAX_ROWS + TIMELINE_TRIM_SLACK ? rows - TIMELINE_MAX_ROWS : 0;
397 for(GList *l = children; l && excess > 0; l = l->next, excess--)
398 {
399 GtkWidget *row = GTK_WIDGET(l->data);
400 const char *hx = (const char *)g_object_get_data(G_OBJECT(row), "svhash");
401 if(hx && g_hash_table_lookup(_g.decl_map, hx) == row) g_hash_table_remove(_g.decl_map, hx);
402 gtk_widget_destroy(row);
403 }
404 g_list_free(children);
405}
406
407static void _clear_list(GtkWidget *list)
408{
409 GList *children = gtk_container_get_children(GTK_CONTAINER(list));
410 for(GList *l = children; l; l = l->next) gtk_widget_destroy(GTK_WIDGET(l->data));
411 g_list_free(children);
412}
413
414static void _update_count(void)
415{
416 gchar *s = g_strdup_printf(_("%u events"), dt_supervisor_events_count());
417 gtk_label_set_text(GTK_LABEL(_g.count_label), s);
418 g_free(s);
419}
420
421static const char *_group_key(const dt_sv_logged_event_t *ev, const char *by)
422{
423 if(!g_strcmp0(by, "thread")) return ev->thread;
424 if(!g_strcmp0(by, "op")) return ev->op;
425 return ev->domain;
426}
427
429{
430 gchar *e = g_markup_escape_text(g->key && *g->key ? g->key : "(none)", -1);
431 gchar *hm = g_strdup_printf("<b>%s</b> <span size=\"small\">(%d)</span>", e, g->count);
432 gtk_label_set_markup(GTK_LABEL(g->header), hm);
433 g_free(e);
434 g_free(hm);
435}
436
437// Append one event into its grouped bucket (creating the bucket if needed),
438// preserving existing groups/rows and their expansion state.
439static void _grouped_append(const dt_sv_logged_event_t *ev, const char *by)
440{
441 const char *k = _group_key(ev, by);
442 _group_t *g = (_group_t *)g_hash_table_lookup(_g.group_map, k);
443 if(!g)
444 {
445 g = g_new0(_group_t, 1);
446 g->key = g_strdup(k);
447 g->body = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
448 gtk_widget_set_margin_start(g->body, 12);
449 GtkWidget *coll = _collapsible("", g->body, FALSE);
450 g->header = (GtkWidget *)g_object_get_data(G_OBJECT(coll), "header");
451 gtk_container_add(GTK_CONTAINER(_g.grouped_list), coll);
452 gtk_widget_show_all(coll);
453 g_hash_table_insert(_g.group_map, g_strdup(k), g);
454 }
456 gtk_box_pack_start(GTK_BOX(g->body), row, FALSE, FALSE, 0);
457 gtk_widget_show_all(row);
458 g->count++;
460}
461
462// Fold all events newer than the grouped cursor into the buckets (live append).
463static void _grouped_catch_up(void)
464{
465 uint64_t newlast = _g.grouped_last_seq;
466 GPtrArray *evs = dt_supervisor_events_snapshot_since(_g.grouped_last_seq, &newlast);
467 if(evs->len)
468 {
469 gchar *by = gtk_combo_box_text_get_active_text(GTK_COMBO_BOX_TEXT(_g.groupby));
470 for(guint i = 0; i < evs->len; i++) _grouped_append(g_ptr_array_index(evs, i), by ? by : "domain");
471 g_free(by);
472 _g.grouped_last_seq = newlast;
473 }
475}
476
477// Full rebuild (group-by change / Clear): wipe buckets and re-fold everything.
478static void _rebuild_grouped_now(void)
479{
480 _clear_list(_g.grouped_list);
481 g_hash_table_remove_all(_g.group_map);
482 _g.grouped_last_seq = 0;
484}
485
486// ---- Memory view -------------------------------------------------------------
487
488// A clickable memory item (markup carries the navigation link). When `trailing`
489// is non-NULL it is packed at the right edge as a separate label, so the trailing
490// column (e.g. vRAM) lines up vertically across rows. An `evictable` item wears the
491// theme's disabled look, without being insensitive: its link stays clickable.
492static void _add_mem_item(GtkWidget *box, const char *markup, const char *trailing, const gboolean evictable)
493{
494 GtkWidget *lbl = gtk_label_new(NULL);
495 gtk_label_set_markup(GTK_LABEL(lbl), markup);
496 gtk_label_set_xalign(GTK_LABEL(lbl), 0.0);
497 /* A long name gives way: every row shares the width of the widest minimum among them, so one
498 * file name too long for the window would push every trailing column out of it. */
499 gtk_label_set_ellipsize(GTK_LABEL(lbl), PANGO_ELLIPSIZE_END);
500 _wire_hash_label(lbl);
501
502 if(trailing && *trailing)
503 {
504 GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
505 gtk_widget_set_margin_start(row, 12);
506 gtk_box_pack_start(GTK_BOX(row), lbl, TRUE, TRUE, 0); // expands → pushes trailing to the right
507 GtkWidget *tl = gtk_label_new(NULL);
508 gtk_label_set_markup(GTK_LABEL(tl), trailing);
509 gtk_label_set_xalign(GTK_LABEL(tl), 1.0);
510 gtk_box_pack_end(GTK_BOX(row), tl, FALSE, FALSE, 0);
511 if(evictable) gtk_style_context_add_class(gtk_widget_get_style_context(row), "dt_supervisor_evictable");
512 gtk_box_pack_start(GTK_BOX(box), row, FALSE, FALSE, 0);
513 }
514 else
515 {
516 gtk_widget_set_margin_start(lbl, 12);
517 if(evictable) gtk_style_context_add_class(gtk_widget_get_style_context(lbl), "dt_supervisor_evictable");
518 gtk_box_pack_start(GTK_BOX(box), lbl, FALSE, FALSE, 0);
519 }
520}
521
522// "12 s", "14 min", "2 h": how long ago, to the unit that keeps it under two digits.
523static gchar *_age_text(const int64_t us)
524{
525 const int64_t s = us / G_USEC_PER_SEC;
526 if(s < 60) return g_strdup_printf("%" G_GINT64_FORMAT " s", s);
527 if(s < 3600) return g_strdup_printf("%" G_GINT64_FORMAT " min", s / 60);
528 return g_strdup_printf("%" G_GINT64_FORMAT " h", s / 3600);
529}
530
531// Held entries first, then each group by what it holds in RAM, descending.
532static gint _cmp_ram_size(gconstpointer a, gconstpointer b)
533{
534 const dt_pixel_cache_stats_entry_t *x = a, *y = b;
535 const gboolean x_held = x->refcount > 0, y_held = y->refcount > 0;
536 if(x_held != y_held) return y_held - x_held;
537 return (y->host_bytes > x->host_bytes) - (y->host_bytes < x->host_bytes);
538}
539
540static gint _cmp_mip_size(gconstpointer a, gconstpointer b)
541{
542 const dt_mipmap_cache_stats_entry_t *x = a, *y = b;
543 return (y->size > x->size) - (y->size < x->size); // descending
544}
545
546static gint _cmp_image_imgid(gconstpointer a, gconstpointer b)
547{
548 const dt_image_cache_stats_entry_t *x = a, *y = b;
549 return x->imgid - y->imgid; // ascending (entries are all the same size)
550}
551
552/* The memory view's bars. A bar is a total and the segments of it something holds, packed from the
553 * left, each in the colour of what holds it -- the same colour in every bar. A segment counted but
554 * not held as such (reserved, or left to others) is hatched; the one left to others sits at the
555 * right end. The track is what is still free. The colours are categorical slots validated against
556 * the window's grey (#686868): none reaches 3:1 on it, so a bar of several segments has a legend
557 * naming each with its size, and every bar a tooltip naming the segment under the pointer. */
558#define BAR_SEGMENTS 4
559#define BAR_SEGMENT_GAP 2.0
560#define COLOR_PIPELINE "#3987e5"
561#define COLOR_ALLOCATIONS "#d95926"
562#define COLOR_MIPMAP "#199e70"
563
564typedef struct _bar_segment_t
565{
566 const char *label;
567 size_t bytes;
568 const char *color; // NULL: neutral
569 gboolean hatched; // counted, but not memory held as such
570 gboolean right; // packed from the right end
571 size_t reserved; // more of it counted, but not held as such: drawn with it, told apart in its text
573
574typedef struct _bar_t
575{
576 size_t total;
577 const char *free_label;
578 int count;
581
582static size_t _bar_segment_size(const _bar_segment_t *seg)
583{
584 return seg->bytes + seg->reserved;
585}
586
587// Every segment, the one left to others included.
588static size_t _bar_used(const _bar_t *bar)
589{
590 size_t used = 0;
591 for(int k = 0; k < bar->count; k++) used += _bar_segment_size(&bar->seg[k]);
592 return used;
593}
594
595// What the bar's owner counts: every segment but the one left to others.
596static size_t _bar_held(const _bar_t *bar)
597{
598 size_t held = 0;
599 for(int k = 0; k < bar->count; k++)
600 if(!bar->seg[k].right) held += _bar_segment_size(&bar->seg[k]);
601 return held;
602}
603
604static size_t _bar_free(const _bar_t *bar)
605{
606 const size_t used = _bar_used(bar);
607 return bar->total > used ? bar->total - used : 0;
608}
609
610// Where segment k lies in a bar `width` wide. Segments that outgrow the total (a budget kernel
611// pressure lowered under its usage) scale the bar to their sum.
612static void _bar_segment_span(const _bar_t *bar, const double width, const int k, double *x0, double *x1)
613{
614 const double scale = width / (double)MAX(bar->total, _bar_used(bar));
615 size_t before = 0;
616 if(bar->seg[k].right)
617 {
618 for(int i = k + 1; i < bar->count; i++)
619 if(bar->seg[i].right) before += _bar_segment_size(&bar->seg[i]);
620 *x1 = width - before * scale;
621 *x0 = *x1 - _bar_segment_size(&bar->seg[k]) * scale;
622 return;
623 }
624 for(int i = 0; i < k; i++)
625 if(!bar->seg[i].right) before += _bar_segment_size(&bar->seg[i]);
626 *x0 = before * scale;
627 *x1 = *x0 + _bar_segment_size(&bar->seg[k]) * scale;
628}
629
630static gboolean _bar_draw(GtkWidget *widget, cairo_t *cr, gpointer user_data)
631{
632 const _bar_t *bar = (const _bar_t *)g_object_get_data(G_OBJECT(widget), "bar");
633 const double width = gtk_widget_get_allocated_width(widget);
634 const double height = gtk_widget_get_allocated_height(widget);
635
636 // The track: what is still free.
637 cairo_set_source_rgba(cr, 1.0, 1.0, 1.0, 0.08);
638 cairo_rectangle(cr, 0.0, 0.0, width, height);
639 cairo_fill(cr);
640 if(IS_NULL_PTR(bar) || (bar->total == 0 && _bar_used(bar) == 0)) return FALSE;
641
642 for(int k = 0; k < bar->count; k++)
643 {
644 const _bar_segment_t *seg = &bar->seg[k];
645 if(_bar_segment_size(seg) == 0) continue;
646 double x0 = 0.0, x1 = 0.0;
647 _bar_segment_span(bar, width, k, &x0, &x1);
648 GdkRGBA c = { 1.0, 1.0, 1.0, 0.45 };
649 if(seg->color) gdk_rgba_parse(&c, seg->color);
650 if(seg->hatched)
651 {
652 cairo_save(cr);
653 cairo_rectangle(cr, x0, 0.0, x1 - x0, height);
654 cairo_clip(cr);
655 if(seg->color)
656 cairo_set_source_rgba(cr, c.red, c.green, c.blue, 0.25);
657 else
658 cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 0.20);
659 cairo_paint(cr);
660 if(seg->color)
661 gdk_cairo_set_source_rgba(cr, &c);
662 else
663 cairo_set_source_rgba(cr, 1.0, 1.0, 1.0, 0.30);
664 cairo_set_line_width(cr, 1.0);
665 for(double x = x0 - height; x < x1; x += 6.0)
666 {
667 cairo_move_to(cr, x, height);
668 cairo_line_to(cr, x + height, 0.0);
669 }
670 cairo_stroke(cr);
671 cairo_restore(cr);
672 continue;
673 }
674 // A surface gap separates a segment from the next; a sliver too thin for one keeps a pixel.
675 gdk_cairo_set_source_rgba(cr, &c);
676 cairo_rectangle(cr, x0, 0.0, MAX(x1 - x0 - BAR_SEGMENT_GAP, 1.0), height);
677 cairo_fill(cr);
678 }
679 return FALSE;
680}
681
682// "12.3 MiB", or "12.3 MiB (4.5 reserved)" for a segment counting more than it holds.
683static gchar *_bar_segment_text(const _bar_segment_t *seg)
684{
685 return seg->reserved
686 ? g_strdup_printf(_("%.1f MiB (%.1f reserved)"), _bar_segment_size(seg) / 1048576.0,
687 seg->reserved / 1048576.0)
688 : g_strdup_printf("%.1f MiB", seg->bytes / 1048576.0);
689}
690
691static gboolean _bar_tooltip(GtkWidget *widget, gint x, gint y, gboolean keyboard, GtkTooltip *tooltip,
692 gpointer user_data)
693{
694 const _bar_t *bar = (const _bar_t *)g_object_get_data(G_OBJECT(widget), "bar");
695 if(IS_NULL_PTR(bar) || bar->total == 0) return FALSE;
696
697 const double width = gtk_widget_get_allocated_width(widget);
698 const _bar_segment_t free_seg = { bar->free_label, _bar_free(bar), NULL, FALSE, FALSE, 0 };
699 const _bar_segment_t *seg = &free_seg;
700 for(int k = 0; k < bar->count; k++)
701 {
702 double x0 = 0.0, x1 = 0.0;
703 _bar_segment_span(bar, width, k, &x0, &x1);
704 if(_bar_segment_size(&bar->seg[k]) == 0 || x < x0 || x >= x1) continue;
705 seg = &bar->seg[k];
706 break;
707 }
708 gchar *size = _bar_segment_text(seg);
709 gchar *text = g_strdup_printf("%s: %s, %.0f%%", seg->label, size,
710 100.0 * (double)_bar_segment_size(seg) / (double)bar->total);
711 gtk_tooltip_set_text(tooltip, text);
712 g_free(size);
713 g_free(text);
714 return TRUE;
715}
716
717/* One legend block: its swatch, then its label with its size on the line below, both lined up
718 * clear of the swatch. Only the swatch wears the colour. */
719static GtkWidget *_bar_legend_block(const _bar_segment_t *seg, const char *swatch)
720{
721 gchar *mark = seg->color ? g_markup_printf_escaped("<span foreground=\"%s\">%s</span>", seg->color, swatch)
722 : g_markup_escape_text(swatch, -1);
723 GtkWidget *swatch_label = gtk_label_new(NULL);
724 gtk_label_set_markup(GTK_LABEL(swatch_label), mark);
725 gtk_widget_set_valign(swatch_label, GTK_ALIGN_START);
726 g_free(mark);
727
728 gchar *size = _bar_segment_text(seg);
729 gchar *text = g_markup_printf_escaped("%s\n%s", seg->label, size);
730 GtkWidget *text_label = gtk_label_new(NULL);
731 gtk_label_set_markup(GTK_LABEL(text_label), text);
732 gtk_label_set_xalign(GTK_LABEL(text_label), 0.0);
733 g_free(size);
734 g_free(text);
735
736 GtkWidget *block = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
737 gtk_box_pack_start(GTK_BOX(block), swatch_label, FALSE, FALSE, 0);
738 gtk_box_pack_start(GTK_BOX(block), text_label, FALSE, FALSE, 0);
739 return block;
740}
741
742// A bar, then its legend.
743static void _add_bar(GtkWidget *box, const _bar_t *bar)
744{
745 GtkWidget *area = gtk_drawing_area_new();
746 gtk_widget_set_size_request(area, -1, 16);
747 _bar_t *copy = g_new(_bar_t, 1);
748 *copy = *bar;
749 g_object_set_data_full(G_OBJECT(area), "bar", copy, g_free);
750 g_signal_connect(area, "draw", G_CALLBACK(_bar_draw), NULL);
751 gtk_widget_set_has_tooltip(area, TRUE);
752 g_signal_connect(area, "query-tooltip", G_CALLBACK(_bar_tooltip), NULL);
753 gtk_box_pack_start(GTK_BOX(box), area, FALSE, FALSE, 0);
754
755 // A single segment needs no legend: the title names it.
756 if(bar->count < 2) return;
757 /* In the bar's order: the segments packed from the left, what is free, then, against the right
758 * edge as in the bar, the segment left to others. */
759 GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 24);
760 GtkWidget *legend = gtk_flow_box_new();
761 gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(legend), GTK_SELECTION_NONE);
762 gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(legend), BAR_SEGMENTS + 1);
763 gtk_flow_box_set_column_spacing(GTK_FLOW_BOX(legend), 24);
764 for(int k = 0; k < bar->count; k++)
765 if(!bar->seg[k].right)
766 gtk_container_add(GTK_CONTAINER(legend), _bar_legend_block(&bar->seg[k], bar->seg[k].hatched ? "▨" : "■"));
767 const _bar_segment_t free_seg = { bar->free_label, _bar_free(bar), NULL, FALSE, FALSE, 0 };
768 gtk_container_add(GTK_CONTAINER(legend), _bar_legend_block(&free_seg, "□"));
769 gtk_box_pack_start(GTK_BOX(row), legend, TRUE, TRUE, 0);
770 for(int k = bar->count - 1; k >= 0; k--)
771 if(bar->seg[k].right)
772 gtk_box_pack_end(GTK_BOX(row), _bar_legend_block(&bar->seg[k], bar->seg[k].hatched ? "▨" : "■"), FALSE,
773 FALSE, 0);
774 gtk_box_pack_start(GTK_BOX(box), row, FALSE, FALSE, 0);
775}
776
777static void _rebuild_memory(void);
778
779static gboolean _rebuild_memory_idle(gpointer user_data)
780{
782 return G_SOURCE_REMOVE;
783}
784
785/* A section is rebuilt at once rather than at the next refresh, folded or opened: what it folds is
786 * built only while it is open. Not from here: the rebuild destroys this expander. */
787// The conf key of a memory section's `setting`: "ui/supervisor/memory/ram/pipeline/folded".
788static gchar *_mem_conf_key(const char *section, const char *setting)
789{
790 return g_strdup_printf("ui/supervisor/memory/%s/%s", section, setting);
791}
792
793static void _on_mem_section_expanded(GObject *expander, GParamSpec *pspec, gpointer user_data)
794{
795 gchar *key = _mem_conf_key((const char *)g_object_get_data(expander, "section"), "folded");
796 dt_conf_set_bool(key, !gtk_expander_get_expanded(GTK_EXPANDER(expander)));
797 g_free(key);
798 g_idle_add(_rebuild_memory_idle, NULL);
799}
800
801/* A section of the memory view, packed in `parent`: its title -- `title` in bold, then what its
802 * bar's owner holds over its total -- with its bar and its legend under it, and what it folds: a list
803 * or the sections of what the memory it shows holds, indented under it. A section without a bar is
804 * named by its title alone. The expander holds the title only and folds nothing itself: holding the
805 * bar too, it centred its arrow on the bar. The view is rebuilt at every refresh, so which sections
806 * are folded is kept by `key`, in the conf so that it outlasts the window, not by their widgets,
807 * and what a folded section folds is not built at all: returns the box it goes in, NULL when
808 * folded. */
809static GtkWidget *_add_mem_section(GtkWidget *parent, const char *key, const char *title, const _bar_t *bar)
810{
811 GtkWidget *hdr = gtk_label_new(NULL);
812 const size_t held = bar ? _bar_held(bar) : 0;
813 gchar *t = bar ? g_markup_printf_escaped("<b>%s</b> %.1f / %.1f MiB (%.0f%%)", title, held / 1048576.0,
814 bar->total / 1048576.0,
815 bar->total ? 100.0 * (double)held / (double)bar->total : 0.0)
816 : g_markup_printf_escaped("<b>%s</b>", title);
817 gtk_label_set_markup(GTK_LABEL(hdr), t);
818 g_free(t);
819 gtk_label_set_xalign(GTK_LABEL(hdr), 0.0);
820 gtk_label_set_ellipsize(GTK_LABEL(hdr), PANGO_ELLIPSIZE_END);
821 // Clear of the expander's arrow, which the theme packs against it.
822 gtk_widget_set_margin_start(hdr, 6);
823
824 // Until the user folds or opens it, a top section is open and what it holds is folded.
825 const gboolean top = parent == _g.mem_box;
826 gchar *folded = _mem_conf_key(key, "folded");
827 const gboolean expanded = dt_conf_key_exists(folded) ? !dt_conf_get_bool(folded) : top;
828 g_free(folded);
829 GtkWidget *expander = gtk_expander_new(NULL);
830 gtk_expander_set_label_widget(GTK_EXPANDER(expander), hdr);
831 gtk_expander_set_label_fill(GTK_EXPANDER(expander), TRUE);
832 gtk_expander_set_expanded(GTK_EXPANDER(expander), expanded);
833 g_object_set_data_full(G_OBJECT(expander), "section", g_strdup(key), g_free);
834 g_signal_connect(expander, "notify::expanded", G_CALLBACK(_on_mem_section_expanded), NULL);
835
836 GtkWidget *section = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
837 gtk_widget_set_margin_top(section, top ? 8 : 6);
838 if(!top) gtk_widget_set_margin_start(section, 16);
839 gtk_box_pack_start(GTK_BOX(section), expander, FALSE, FALSE, 0);
840 gtk_box_pack_start(GTK_BOX(parent), section, FALSE, FALSE, 0);
841 if(bar)
842 {
843 /* Lined up with the title's text: what the expander asks beyond its label is its arrow and their
844 * spacing, as wide as the theme makes them, and the label's own margin comes on top. A hidden
845 * widget asks nothing, and the theme reaches them once they are in the window. */
846 gtk_widget_show(expander);
847 gtk_widget_show(hdr);
848 int expander_width = 0, label_width = 0;
849 gtk_widget_get_preferred_width(expander, NULL, &expander_width);
850 gtk_widget_get_preferred_width(hdr, NULL, &label_width);
851 GtkWidget *bar_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
852 gtk_widget_set_margin_start(bar_box, MAX(expander_width - label_width, 0) + gtk_widget_get_margin_start(hdr));
853 _add_bar(bar_box, bar);
854 gtk_box_pack_start(GTK_BOX(section), bar_box, FALSE, FALSE, 0);
855 }
856 if(!expanded) return NULL;
857
858 GtkWidget *body = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
859 gtk_box_pack_start(GTK_BOX(section), body, FALSE, FALSE, 0);
860 return body;
861}
862
863/* A section's list: a recessed area of a fixed height, which the user changes by the grip on its
864 * bottom edge, kept in the conf. Fitted to its rows, a list first seen empty stayed one row tall
865 * as it filled. The view is rebuilt at every refresh, but not the area: kept by its section,
866 * unpacked before the rebuild and packed again around new rows, it keeps its height and where it
867 * was scrolled. Its rows are swapped between two allocations, so its scroll position is never
868 * clamped to an empty list. Returns the box the rows go in, emptied. */
869static GtkWidget *_mem_list(GtkWidget *body, const char *section)
870{
871 GtkWidget *area = (GtkWidget *)g_hash_table_lookup(_g.mem_lists, section);
872 if(IS_NULL_PTR(area))
873 {
874 GtkWidget *list = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
875 gchar *key = _mem_conf_key(section, "height");
876 /* Until the grip sets it, MEMORY_LIST_ROWS rows. The list may be empty yet, so a row is measured
877 * on a probe built like one, in the window, where the theme reaches it. */
879 {
880 GtkWidget *probe = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
881 gtk_box_pack_start(GTK_BOX(body), probe, FALSE, FALSE, 0);
882 _add_mem_item(probe, "<a href=\"0\"><tt>0x0</tt></a> <i>0</i>", "<tt>0</tt>", FALSE);
883 // A hidden widget asks for nothing.
884 gtk_widget_show_all(probe);
885 int row = 0;
886 gtk_widget_get_preferred_height(probe, NULL, &row);
887 gtk_widget_destroy(probe);
888 if(row > 0) dt_conf_set_int(key, MEMORY_LIST_ROWS * (row + gtk_box_get_spacing(GTK_BOX(list))));
889 }
890 area = dt_ui_scroll_wrap(list, 20, key, DT_UI_RESIZE_STATIC);
891 g_free(key);
892 dt_gui_add_class(dt_ui_scroll_wrap_get_scrolled_window(area), "dt_recessed_scroll");
893 g_object_set_data(G_OBJECT(area), "list", list);
894 g_hash_table_insert(_g.mem_lists, g_strdup(section), g_object_ref_sink(area));
895 }
896 GtkWidget *list = (GtkWidget *)g_object_get_data(G_OBJECT(area), "list");
897 _clear_list(list);
898 gtk_box_pack_start(GTK_BOX(body), area, FALSE, FALSE, 0);
899 return list;
900}
901
902// Held lines first, then each group by what it holds on the device passed in `data`, descending.
903static gint _cmp_device_size(gconstpointer a, gconstpointer b, gpointer data)
904{
905 const dt_pixel_cache_stats_entry_t *x = a, *y = b;
906 const int devid = GPOINTER_TO_INT(data);
907 const gboolean x_held = x->refcount > 0, y_held = y->refcount > 0;
908 if(x_held != y_held) return y_held - x_held;
909 const size_t xb = x->devices[devid].cl_bytes, yb = y->devices[devid].cl_bytes;
910 return (yb > xb) - (yb < xb);
911}
912
913/* One OpenCL device: a bar over its memory, then, under it, the pipeline lines holding buffers on
914 * it. Ansel's own accounting gives what it holds there; what the lines hold is one part of that,
915 * everything else -- modules' working buffers, and what they keep there for longer -- the other.
916 * The device's headroom, what Ansel leaves to other applications there, is hatched at the right
917 * end. What they actually hold is not known to OpenCL, so the track counts it as free. */
918static void _add_device_section(GArray *pe, const int devid)
919{
920 size_t lines = 0;
921 for(guint i = 0; i < pe->len; i++)
922 lines += g_array_index(pe, dt_pixel_cache_stats_entry_t, i).devices[devid].cl_bytes;
923 // Read apart from the lines' buffers, hence the clamp.
924 const size_t in_use = dt_opencl_get_device_memory_in_use(devid);
925 const _bar_t bar = {
927 .free_label = _("free in the budget"),
928 .count = 3,
929 .seg = { { _("pipeline cache, lines in vRAM"), lines, COLOR_PIPELINE, FALSE, FALSE },
930 { _("allocations outside the lines"), in_use > lines ? in_use - lines : 0, COLOR_ALLOCATIONS, FALSE,
931 FALSE },
932 { _("headroom for display and applications"), dt_opencl_get_device_headroom(devid), NULL, TRUE,
933 TRUE } },
934 };
935 const char *name = dt_opencl_get_device_name(devid);
936 gchar *title = name ? g_strdup_printf(_("vRAM — %s"), name) : g_strdup_printf(_("vRAM — device %d"), devid);
937 gchar *key = g_strdup_printf("vram-%d", devid);
938 GtkWidget *body = _add_mem_section(_g.mem_box, key, title, &bar);
939 g_free(key);
940 g_free(title);
941 if(!body) return;
942
943 // Its size is the device bar's first segment.
944 guint count = 0;
945 for(guint i = 0; i < pe->len; i++)
946 if(g_array_index(pe, dt_pixel_cache_stats_entry_t, i).devices[devid].cl_count) count++;
947 gchar *ltitle = g_strdup_printf(_("Pipeline cache — %u items"), count);
948 key = g_strdup_printf("vram-%d/pipeline", devid);
949 GtkWidget *lbody = _add_mem_section(body, key, ltitle, NULL);
950 g_free(ltitle);
951 if(!lbody)
952 {
953 g_free(key);
954 return;
955 }
956 GtkWidget *list = _mem_list(lbody, key);
957
958 g_array_sort_with_data(pe, _cmp_device_size, GINT_TO_POINTER(devid));
959 guint shown = 0;
960 for(guint i = 0; i < pe->len && shown < MEMORY_MAX_ROWS; i++)
961 {
962 const dt_pixel_cache_stats_entry_t *e = &g_array_index(pe, dt_pixel_cache_stats_entry_t, i);
963 const dt_pixel_cache_stats_device_t *d = &e->devices[devid];
964 if(d->cl_count == 0) continue;
965 gchar *hx = _hashhex(e->hash);
966 gchar *ename = g_markup_escape_text(e->name[0] ? e->name : "-", -1);
967 gchar *m = g_strdup_printf("<a href=\"%s\"><tt>%s</tt></a> refs=%d hits=%d <i>%s</i>", hx, hx, e->refcount,
968 e->hits, ename);
969 gchar *column = g_strdup_printf("<tt>%8.2f MiB %2d buf</tt>", d->cl_bytes / 1048576.0, d->cl_count);
970 _add_mem_item(list, m, column, e->refcount == 0);
971 g_free(hx);
972 g_free(ename);
973 g_free(m);
974 g_free(column);
975 shown++;
976 }
977 g_free(key);
978}
979
980static void _rebuild_memory(void)
981{
982 if(!_g.mem_box) return;
983 // Out of the sections first: destroying a container destroys its children, referenced or not.
984 GHashTableIter it;
985 gpointer area = NULL;
986 g_hash_table_iter_init(&it, _g.mem_lists);
987 while(g_hash_table_iter_next(&it, NULL, &area))
988 {
989 GtkWidget *parent = gtk_widget_get_parent(GTK_WIDGET(area));
990 if(parent) gtk_container_remove(GTK_CONTAINER(parent), GTK_WIDGET(area));
991 }
992 _clear_list(_g.mem_box);
993
994 // Pipeline cache
995 size_t cur = 0, max = 0;
998
999 /* The budget is not all RAM: a line kept on the device only reserves its size without a host
1000 * buffer. What modules allocate there -- working buffers for one run, or lookup tables and state
1001 * kept for longer -- counts in it without being lines. Lines and allocations are what the usage
1002 * sums, read under one hold of the cache lock. */
1003 const gboolean gpu = dt_opencl_is_enabled();
1004 size_t in_ram = 0, reserved = 0, allocated = 0;
1005 guint lines = 0;
1006 for(guint i = 0; i < pe->len; i++)
1007 {
1008 const dt_pixel_cache_stats_entry_t *e = &g_array_index(pe, dt_pixel_cache_stats_entry_t, i);
1009 if(e->allocation)
1010 {
1011 allocated += e->size;
1012 continue;
1013 }
1014 in_ram += e->host_bytes;
1015 reserved += e->size - e->host_bytes;
1016 lines++;
1017 }
1018
1019 size_t mip_cur = 0, mip_max = 0;
1020 dt_mipmap_cache_get_usage(&mip_cur, &mip_max);
1021
1022 /* The startup plan gives the OS and other applications their headroom (preferences), the
1023 * mipmap cache its budget, and the pipeline cache the rest -- so the system's share is what
1024 * the two cache budgets leave of the total. */
1025 const size_t total = dt_get_total_mem();
1026 const size_t budgets = mip_max + dt_dev_pixelpipe_cache_get_max_memory();
1027 const _bar_t ram_bar = {
1028 .total = total,
1029 .free_label = _("free in the cache budgets"),
1030 .count = 3,
1031 /* Its lines in RAM and its allocations, and what lines kept in vRAM only reserve of its
1032 * budget: no RAM, but the cache evicts and refuses on it as on the rest. The bar below tells
1033 * the three apart. */
1034 .seg = { { _("pipeline cache (RAM)"), in_ram + allocated, COLOR_PIPELINE, FALSE, FALSE, reserved },
1035 { _("mipmap cache"), mip_cur, COLOR_MIPMAP, FALSE, FALSE },
1036 { _("headroom for OS and applications"), total > budgets ? total - budgets : 0, NULL, TRUE, TRUE } },
1037 };
1038 // The caches it holds, under it; each device, with what it holds, after it.
1039 GtkWidget *rbody = _add_mem_section(_g.mem_box, "ram", _("RAM"), &ram_bar);
1040
1041 /* The budget is not all RAM: a line an OpenCL module keeps on the device only reserves its size,
1042 * so it is hatched in the pipeline cache's colour. */
1043 const _bar_t pipeline = {
1044 .total = max,
1045 .free_label = _("free in the budget"),
1046 .count = gpu ? 3 : 2,
1047 .seg = { { gpu ? _("lines in RAM") : _("lines"), in_ram, COLOR_PIPELINE, FALSE, FALSE },
1048 { _("allocations outside the lines"), allocated, COLOR_ALLOCATIONS, FALSE, FALSE },
1049 { _("reserved by lines in vRAM only"), reserved, COLOR_PIPELINE, TRUE, FALSE } },
1050 };
1051 gchar *ptitle = g_strdup_printf(gpu ? _("Pipeline cache (RAM budget) — %u items") : _("Pipeline cache — %u items"),
1052 lines);
1053 GtkWidget *pbody = rbody ? _add_mem_section(rbody, "ram/pipeline", ptitle, &pipeline) : NULL;
1054 g_free(ptitle);
1055
1056 /* What the pipeline cache holds in RAM: its lines' host buffers and the allocations, each after a
1057 * swatch in its segment's colour. A line kept on a device only is listed under that device alone;
1058 * one with buffers in both memories, under both, each with what it holds there. The age tells a
1059 * working buffer, which lives for one module's run, from what is kept for longer. */
1060 if(pbody)
1061 {
1062 GtkWidget *list = _mem_list(pbody, "ram/pipeline");
1063 g_array_sort(pe, _cmp_ram_size);
1064 guint shown = 0;
1065 for(guint i = 0; i < pe->len && shown < MEMORY_MAX_ROWS; i++)
1066 {
1067 const dt_pixel_cache_stats_entry_t *e = &g_array_index(pe, dt_pixel_cache_stats_entry_t, i);
1068 if(e->host_bytes == 0) continue;
1069 gchar *name = g_markup_escape_text(e->name[0] ? e->name : "-", -1);
1070 /* A fixed-width monospace column, so the sizes line up across rows. A stale host buffer
1071 * occupies RAM but holds the pixels of another hash. The age is one phrase, aligned right: padded
1072 * between its verb and its duration, the verb read as a column title. */
1073 gchar *age = _age_text(e->age_us);
1074 gchar *when = g_strdup_printf("%s %s ago", e->allocation ? "allocated" : "used", age);
1075 gchar *column = g_strdup_printf("<tt>%8.2f MiB%s %20s</tt>", e->host_bytes / 1048576.0,
1076 e->host_stale ? " stale" : " ", when);
1077 g_free(age);
1078 g_free(when);
1079 if(e->allocation)
1080 {
1081 // Named after where it was allocated; its hash is its address, which no event names.
1082 gchar *m = g_strdup_printf("<span foreground=\"%s\">■</span> %s <i>%s</i>", COLOR_ALLOCATIONS,
1083 _("allocation"), name);
1084 _add_mem_item(list, m, column, FALSE);
1085 g_free(m);
1086 }
1087 else
1088 {
1089 gchar *hx = _hashhex(e->hash);
1090 gchar *m = g_strdup_printf("<span foreground=\"%s\">■</span> <a href=\"%s\"><tt>%s</tt></a> refs=%d "
1091 "hits=%d <i>%s</i>",
1092 COLOR_PIPELINE, hx, hx, e->refcount, e->hits, name);
1093 /* A line nobody holds is what the LRU, the pressure valve and the flushes may free. They
1094 * also skip a line someone has locked, but whoever writes or reads a line holds it too. */
1095 _add_mem_item(list, m, column, e->refcount == 0);
1096 g_free(hx);
1097 g_free(m);
1098 }
1099 g_free(name);
1100 g_free(column);
1101 shown++;
1102 }
1103 }
1104
1105 // Mipmap cache
1106 GArray *me = dt_mipmap_cache_get_entries_stats();
1107 const _bar_t mipmap = {
1108 .total = mip_max,
1109 .free_label = _("free in the budget"),
1110 .count = 1,
1111 .seg = { { _("mipmap cache"), mip_cur, COLOR_MIPMAP, FALSE, FALSE } },
1112 };
1113 gchar *mtitle = g_strdup_printf(_("Mipmap cache — %u items"), me->len);
1114 GtkWidget *mbody = rbody ? _add_mem_section(rbody, "ram/mipmap", mtitle, &mipmap) : NULL;
1115 g_free(mtitle);
1116 GtkWidget *mlist = mbody ? _mem_list(mbody, "ram/mipmap") : NULL;
1117 if(mlist) g_array_sort(me, _cmp_mip_size);
1118 for(guint i = 0; mlist && i < me->len && i < MEMORY_MAX_ROWS; i++)
1119 {
1120 const dt_mipmap_cache_stats_entry_t *e = &g_array_index(me, dt_mipmap_cache_stats_entry_t, i);
1121 gchar *hx = _hashhex(dt_supervisor_mipmap_key(e->imgid, e->mip));
1122 gchar *m = g_strdup_printf("<a href=\"%s\">image #%d · mip %d</a> %.2f MiB", hx, e->imgid, e->mip,
1123 e->size / 1048576.0);
1124 _add_mem_item(mlist, m, NULL, FALSE);
1125 g_free(hx);
1126 g_free(m);
1127 }
1128 g_array_free(me, TRUE);
1129
1130 // Image cache
1132 GArray *ie = dt_image_cache_get_entries_stats();
1133 // Not in the RAM bar: its colour is neutral.
1134 const _bar_t image = {
1135 .total = max,
1136 .free_label = _("free in the budget"),
1137 .count = 1,
1138 .seg = { { _("image cache"), cur, NULL, FALSE, FALSE } },
1139 };
1140 gchar *ititle = g_strdup_printf(_("Image cache — %u items"), ie->len);
1141 GtkWidget *ibody = rbody ? _add_mem_section(rbody, "ram/image", ititle, &image) : NULL;
1142 g_free(ititle);
1143 GtkWidget *ilist = ibody ? _mem_list(ibody, "ram/image") : NULL;
1144 if(ilist) g_array_sort(ie, _cmp_image_imgid);
1145 for(guint i = 0; ilist && i < ie->len && i < MEMORY_MAX_ROWS; i++)
1146 {
1147 const dt_image_cache_stats_entry_t *e = &g_array_index(ie, dt_image_cache_stats_entry_t, i);
1148 gchar *hx = _hashhex(dt_supervisor_image_key(e->imgid));
1149 gchar *fn = g_markup_escape_text(e->filename[0] ? e->filename : "-", -1);
1150 gchar *m = g_strdup_printf("<a href=\"%s\">image #%d</a> <i>%s</i>", hx, e->imgid, fn);
1151 _add_mem_item(ilist, m, NULL, FALSE);
1152 g_free(hx);
1153 g_free(fn);
1154 g_free(m);
1155 }
1156 g_array_free(ie, TRUE);
1157
1158 if(gpu)
1159 for(int devid = 0; devid < dt_opencl_get_num_devices(); devid++) _add_device_section(pe, devid);
1160 g_array_free(pe, TRUE);
1161
1162 gtk_widget_show_all(_g.mem_box);
1163}
1164
1165// ---- Search ------------------------------------------------------------------
1166
1167// TRUE if the hash's hex (lowercased, with 0x) contains the needle.
1168static gboolean _hex_contains(const uint64_t h, const char *needle)
1169{
1170 gchar *hx = _hashhex(h);
1171 gchar *lh = g_ascii_strdown(hx, -1);
1172 const gboolean m = strstr(lh, needle) != NULL;
1173 g_free(hx);
1174 g_free(lh);
1175 return m;
1176}
1177
1178// An event matches when:
1179// - its own hash or any linked hash contains `hash_needle` (hex, 0x stripped), or
1180// - its full record (JSON: module/op/domain/widget/filename/parameters/…)
1181// contains `text_needle` as a case-insensitive substring.
1182static gboolean _event_matches(const dt_sv_logged_event_t *ev, const char *hash_needle,
1183 const char *text_needle)
1184{
1185 if(*hash_needle)
1186 {
1187 if(_hex_contains(ev->hash, hash_needle)) return TRUE;
1188 if(ev->links)
1189 for(guint i = 0; i < ev->links->len; i++)
1190 if(_hex_contains(g_array_index(ev->links, dt_sv_link_t, i).hash, hash_needle)) return TRUE;
1191 }
1192 if(*text_needle && ev->json)
1193 {
1194 gchar *jl = g_ascii_strdown(ev->json, -1);
1195 const gboolean m = strstr(jl, text_needle) != NULL;
1196 g_free(jl);
1197 if(m) return TRUE;
1198 }
1199 return FALSE;
1200}
1201
1202static void _run_search(const char *query)
1203{
1204 if(!_g.search_list) return;
1205 _clear_list(_g.search_list);
1206 if(!query || !*query)
1207 {
1208 gtk_widget_show_all(_g.search_list);
1209 return;
1210 }
1211
1212 // text_needle matches any field of the record; hash_needle (0x stripped) matches hashes.
1213 gchar *text_needle = g_ascii_strdown(query, -1);
1214 const char *hash_needle = g_str_has_prefix(text_needle, "0x") ? text_needle + 2 : text_needle;
1215
1216 GPtrArray *evs = dt_supervisor_events_snapshot();
1217 for(guint i = 0; i < evs->len; i++)
1218 {
1219 const dt_sv_logged_event_t *ev = g_ptr_array_index(evs, i);
1220 if(_event_matches(ev, hash_needle, text_needle))
1221 gtk_container_add(GTK_CONTAINER(_g.search_list), _event_widget(ev));
1222 }
1224 g_free(text_needle);
1225 gtk_widget_show_all(_g.search_list);
1226}
1227
1228static void _on_search_changed(GtkSearchEntry *e, gpointer u)
1229{
1230 const char *txt = gtk_entry_get_text(GTK_ENTRY(e));
1231 // Typing a query jumps to the Search page automatically.
1232 if(txt && *txt) gtk_notebook_set_current_page(GTK_NOTEBOOK(_g.notebook), _g.page_search);
1233 _run_search(txt);
1234}
1235
1236static gboolean _scroll_bottom_idle(gpointer u)
1237{
1238 if(!_g.timeline_scroll) return G_SOURCE_REMOVE;
1239 GtkAdjustment *adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(_g.timeline_scroll));
1240 if(adj) gtk_adjustment_set_value(adj, gtk_adjustment_get_upper(adj));
1241 return G_SOURCE_REMOVE;
1242}
1243
1244/* A mouse button held over the window: a drag in progress, maybe on a list's grip, which a rebuild
1245 * of the memory view would cut short by unpacking the grip under the pointer. */
1246static gboolean _pointer_button_down(void)
1247{
1248 GdkWindow *window = gtk_widget_get_window(_g.window);
1249 if(IS_NULL_PTR(window)) return FALSE;
1250 GdkDevice *pointer = gdk_seat_get_pointer(gdk_display_get_default_seat(gdk_window_get_display(window)));
1251 GdkModifierType mask = 0;
1252 gdk_window_get_device_position(window, pointer, NULL, NULL, &mask);
1253 return (mask & (GDK_BUTTON1_MASK | GDK_BUTTON2_MASK | GDK_BUTTON3_MASK)) != 0;
1254}
1255
1256static gboolean _poll(gpointer u)
1257{
1258 if(!_g.window) return G_SOURCE_REMOVE;
1259
1260 _g.tick++;
1261 const int page = gtk_notebook_get_current_page(GTK_NOTEBOOK(_g.notebook));
1262 // The grouped view appends new events live (incrementally, preserving folds)
1263 // while it is the visible page.
1264 if(page == _g.page_grouped) _grouped_catch_up();
1265 // The memory view reflects live cache state; refresh it on a throttled cadence.
1266 // The search view is intentionally NOT auto-refreshed here: rebuilding it would
1267 // collapse any rows the user expanded. It updates on input / Refresh instead.
1268 if((_g.tick % MEMORY_REFRESH_TICKS) == 0 && page == _g.page_memory && !_pointer_button_down())
1270
1271 uint64_t newlast = _g.last_seq;
1272 GPtrArray *evs = dt_supervisor_events_snapshot_since(_g.last_seq, &newlast);
1273 if(evs->len)
1274 {
1275 GtkAdjustment *adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(_g.timeline_scroll));
1276 const gboolean at_bottom = !adj
1277 || gtk_adjustment_get_value(adj)
1278 >= gtk_adjustment_get_upper(adj) - gtk_adjustment_get_page_size(adj) - 4.0;
1279
1280 /* Appending a row to a list box in the window costs GTK 3 time in proportion to the rows
1281 * already there, so a batch never takes the list past its cap and slack, even for a moment:
1282 * older events would be trimmed right away. A large batch -- the whole log, when the window
1283 * opens -- is built with the list out of the window, where every row costs the same; below
1284 * half the cap, appending in place is cheaper. Measured on a mock with the theme: 4000 rows
1285 * appended in place 4.2 s, built out of the window 2.9 s; a 12500-row batch in place hung
1286 * the GUI thread. */
1287 const guint first = evs->len > TIMELINE_MAX_ROWS ? evs->len - TIMELINE_MAX_ROWS : 0;
1288 const guint incoming = evs->len - first;
1289 GtkWidget *viewport = incoming > TIMELINE_MAX_ROWS / 2 ? gtk_widget_get_parent(_g.timeline_list) : NULL;
1290 if(viewport)
1291 {
1292 g_object_ref(_g.timeline_list);
1293 gtk_container_remove(GTK_CONTAINER(viewport), _g.timeline_list);
1294 }
1295 _timeline_trim(incoming);
1296 for(guint i = first; i < evs->len; i++) _timeline_append(g_ptr_array_index(evs, i));
1297 if(viewport)
1298 {
1299 gtk_container_add(GTK_CONTAINER(viewport), _g.timeline_list);
1300 g_object_unref(_g.timeline_list);
1301 }
1302 _g.last_seq = newlast;
1303 _update_count();
1304 if(at_bottom) g_idle_add(_scroll_bottom_idle, NULL);
1305 }
1307 return G_SOURCE_CONTINUE;
1308}
1309
1310static void _full_reload(void)
1311{
1312 _clear_list(_g.timeline_list);
1313 g_hash_table_remove_all(_g.decl_map);
1314 _g.last_seq = 0;
1315 _poll(NULL); // appends everything since the start
1318 _run_search(gtk_entry_get_text(GTK_ENTRY(_g.search_entry)));
1319}
1320
1321static void _on_refresh(GtkButton *b, gpointer u) { _full_reload(); }
1322
1323static void _on_clear(GtkButton *b, gpointer u)
1324{
1326 _full_reload();
1327}
1328
1329/* The page shown, the group-by key and the Record box, kept for the next time the window opens. The
1330 * page is kept by name, not by index, so that it survives a change in the order of the tabs. */
1331#define SUPERVISOR_CONF_PAGE "ui/supervisor/page"
1332#define SUPERVISOR_CONF_GROUP_BY "ui/supervisor/group_by"
1333#define SUPERVISOR_CONF_RECORD "ui/supervisor/record"
1334
1335static const char *_page_name(const int page)
1336{
1337 if(page == _g.page_timeline) return "timeline";
1338 if(page == _g.page_grouped) return "grouped";
1339 if(page == _g.page_search) return "search";
1340 return "memory";
1341}
1342
1343static int _page_from_name(const char *name)
1344{
1345 if(!g_strcmp0(name, "timeline")) return _g.page_timeline;
1346 if(!g_strcmp0(name, "grouped")) return _g.page_grouped;
1347 if(!g_strcmp0(name, "search")) return _g.page_search;
1348 return _g.page_memory;
1349}
1350
1351static void _on_groupby_changed(GtkComboBox *c, gpointer u)
1352{
1353 gchar *by = gtk_combo_box_text_get_active_text(GTK_COMBO_BOX_TEXT(c));
1355 g_free(by);
1356 _rebuild_grouped_now(); // regroup everything under the new key
1357}
1358
1359static void _on_record_toggled(GtkToggleButton *t, gpointer u)
1360{
1361 const gboolean active = gtk_toggle_button_get_active(t);
1364}
1365
1366static void _on_page_changed(GtkNotebook *nb, GtkWidget *page, guint page_num, gpointer u)
1367{
1369 if((int)page_num == _g.page_grouped)
1370 _grouped_catch_up(); // fold in whatever arrived while the page was hidden
1371 else if((int)page_num == _g.page_memory)
1373 else if((int)page_num == _g.page_search)
1374 _run_search(gtk_entry_get_text(GTK_ENTRY(_g.search_entry)));
1375}
1376
1377/* Where the user left the window and how large they made it. */
1378#define SUPERVISOR_CONF_X "ui/supervisor/window_x"
1379#define SUPERVISOR_CONF_Y "ui/supervisor/window_y"
1380#define SUPERVISOR_CONF_WIDTH "ui/supervisor/window_width"
1381#define SUPERVISOR_CONF_HEIGHT "ui/supervisor/window_height"
1382
1383// Wayland gives a client neither its own position nor the right to set it: there, the size only.
1385{
1386#ifdef GDK_WINDOWING_WAYLAND
1387 return !GDK_IS_WAYLAND_DISPLAY(gtk_widget_get_display(window));
1388#else
1389 return TRUE;
1390#endif
1391}
1392
1393/* Kept as the user moves and resizes the window, not when it closes: Ansel can quit with it open.
1394 * A maximized or fullscreen window has no geometry of its own to keep. */
1395static gboolean _on_configure(GtkWidget *window, GdkEventConfigure *event, gpointer user_data)
1396{
1397 GdkWindow *gdk_window = gtk_widget_get_window(window);
1398 if(IS_NULL_PTR(gdk_window)
1399 || (gdk_window_get_state(gdk_window) & (GDK_WINDOW_STATE_MAXIMIZED | GDK_WINDOW_STATE_FULLSCREEN)))
1400 return FALSE;
1401
1402 int width = 0, height = 0;
1403 gtk_window_get_size(GTK_WINDOW(window), &width, &height);
1407
1408 int x = 0, y = 0;
1409 gtk_window_get_position(GTK_WINDOW(window), &x, &y);
1412 return FALSE;
1413}
1414
1415/* Puts the window back as it was left, before it is mapped. With nothing stored, the default size,
1416 * and the window manager places it. A position saved on a monitor no longer attached would strand
1417 * the window off screen, so it is clamped into the work area of the monitor it lands on. */
1419{
1420 int width = 1000, height = 640;
1422 {
1425 }
1426 gtk_window_set_default_size(GTK_WINDOW(window), width, height);
1427
1430 return;
1431
1434 GdkDisplay *display = gtk_widget_get_display(window);
1435 GdkMonitor *monitor = gdk_display_get_monitor_at_point(display, x + width / 2, y + height / 2);
1436 if(IS_NULL_PTR(monitor)) monitor = gdk_display_get_primary_monitor(display);
1437 if(IS_NULL_PTR(monitor) && gdk_display_get_n_monitors(display) > 0) monitor = gdk_display_get_monitor(display, 0);
1438 if(!IS_NULL_PTR(monitor))
1439 {
1440 GdkRectangle workarea = { 0 };
1441 gdk_monitor_get_workarea(monitor, &workarea);
1442 x = CLAMP(x, workarea.x, workarea.x + MAX(0, workarea.width - width));
1443 y = CLAMP(y, workarea.y, workarea.y + MAX(0, workarea.height - height));
1444 }
1445 gtk_window_move(GTK_WINDOW(window), x, y);
1446}
1447
1448static void _on_destroy(GtkWidget *w, gpointer u)
1449{
1450 if(_g.timer_id) g_source_remove(_g.timer_id);
1451 dt_supervisor_set_recording(FALSE); // stop capturing once the viewer is gone
1452 if(_g.decl_map) g_hash_table_destroy(_g.decl_map);
1453 if(_g.group_map) g_hash_table_destroy(_g.group_map);
1454 if(_g.mem_lists) g_hash_table_destroy(_g.mem_lists);
1455 g_free(_g.scroll_target);
1456 memset(&_g, 0, sizeof(_g));
1457}
1458
1460{
1461 if(_g.window)
1462 {
1463 gtk_window_present(GTK_WINDOW(_g.window));
1464 return;
1465 }
1466
1467 _g.decl_map = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
1468 _g.group_map = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, _group_free);
1469 _g.mem_lists = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_object_unref);
1470
1471 _g.window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
1472 gtk_window_set_title(GTK_WINDOW(_g.window), _("Event supervisor"));
1473 gtk_window_set_transient_for(GTK_WINDOW(_g.window), GTK_WINDOW(dt_gui_main_window()));
1474 _restore_geometry(_g.window);
1475 g_signal_connect(_g.window, "configure-event", G_CALLBACK(_on_configure), NULL);
1476 g_signal_connect(_g.window, "destroy", G_CALLBACK(_on_destroy), NULL);
1477
1478 GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
1479 gtk_container_set_border_width(GTK_CONTAINER(vbox), 8);
1480 gtk_container_add(GTK_CONTAINER(_g.window), vbox);
1481
1482 // toolbar
1483 GtkWidget *bar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
1484 gtk_box_pack_start(GTK_BOX(vbox), bar, FALSE, FALSE, 0);
1485
1486 GtkWidget *record = gtk_check_button_new_with_label(_("Record"));
1487 // Capturing on open unless the user unticked it last time.
1489 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(record), recording);
1490 dt_supervisor_set_recording(recording);
1491 g_signal_connect(record, "toggled", G_CALLBACK(_on_record_toggled), NULL);
1492 gtk_box_pack_start(GTK_BOX(bar), record, FALSE, FALSE, 0);
1493
1494 GtkWidget *refresh = gtk_button_new_with_label(_("Refresh"));
1495 g_signal_connect(refresh, "clicked", G_CALLBACK(_on_refresh), NULL);
1496 gtk_box_pack_start(GTK_BOX(bar), refresh, FALSE, FALSE, 0);
1497
1498 GtkWidget *clear = gtk_button_new_with_label(_("Clear"));
1499 g_signal_connect(clear, "clicked", G_CALLBACK(_on_clear), NULL);
1500 gtk_box_pack_start(GTK_BOX(bar), clear, FALSE, FALSE, 0);
1501
1502 gtk_box_pack_start(GTK_BOX(bar), gtk_label_new(_("Group by")), FALSE, FALSE, 0);
1503 _g.groupby = gtk_combo_box_text_new();
1504 gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(_g.groupby), "domain");
1505 gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(_g.groupby), "thread");
1506 gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(_g.groupby), "op");
1507 // The combo's ids are its texts, so the kept key selects its entry; an unknown one falls back to domain.
1508 gtk_combo_box_set_id_column(GTK_COMBO_BOX(_g.groupby), 0);
1510 || !gtk_combo_box_set_active_id(GTK_COMBO_BOX(_g.groupby), dt_conf_get_string_const(SUPERVISOR_CONF_GROUP_BY)))
1511 gtk_combo_box_set_active(GTK_COMBO_BOX(_g.groupby), 0);
1512 g_signal_connect(_g.groupby, "changed", G_CALLBACK(_on_groupby_changed), NULL);
1513 gtk_box_pack_start(GTK_BOX(bar), _g.groupby, FALSE, FALSE, 0);
1514
1515 // global search entry: query all events by hash (own or linked)
1516 _g.search_entry = gtk_search_entry_new();
1517 gtk_entry_set_placeholder_text(GTK_ENTRY(_g.search_entry), _("search hash or text…"));
1518 gtk_widget_set_size_request(_g.search_entry, 220, -1);
1519 g_signal_connect(_g.search_entry, "search-changed", G_CALLBACK(_on_search_changed), NULL);
1520 gtk_box_pack_end(GTK_BOX(bar), _g.search_entry, FALSE, FALSE, 0);
1521
1522 _g.count_label = gtk_label_new("");
1523 gtk_box_pack_end(GTK_BOX(bar), _g.count_label, FALSE, FALSE, 0);
1524
1525 // notebook with the four pages, tabs spread across the full width
1526 _g.notebook = gtk_notebook_new();
1527 gtk_box_pack_start(GTK_BOX(vbox), _g.notebook, TRUE, TRUE, 0);
1528
1529 _g.mem_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
1530 gtk_container_set_border_width(GTK_CONTAINER(_g.mem_box), 6);
1531 GtkWidget *sw3 = gtk_scrolled_window_new(NULL, NULL);
1532 gtk_container_add(GTK_CONTAINER(sw3), _g.mem_box);
1533 _g.page_memory = gtk_notebook_append_page(GTK_NOTEBOOK(_g.notebook), sw3, gtk_label_new(_("Memory")));
1534
1535 _g.timeline_list = gtk_list_box_new();
1536 gtk_list_box_set_selection_mode(GTK_LIST_BOX(_g.timeline_list), GTK_SELECTION_SINGLE);
1537 _g.timeline_scroll = gtk_scrolled_window_new(NULL, NULL);
1538 gtk_container_add(GTK_CONTAINER(_g.timeline_scroll), _g.timeline_list);
1539 _g.page_timeline = gtk_notebook_append_page(GTK_NOTEBOOK(_g.notebook), _g.timeline_scroll,
1540 gtk_label_new(_("Timeline")));
1541
1542 _g.grouped_list = gtk_list_box_new();
1543 gtk_list_box_set_selection_mode(GTK_LIST_BOX(_g.grouped_list), GTK_SELECTION_NONE);
1544 GtkWidget *sw2 = gtk_scrolled_window_new(NULL, NULL);
1545 gtk_container_add(GTK_CONTAINER(sw2), _g.grouped_list);
1546 _g.page_grouped = gtk_notebook_append_page(GTK_NOTEBOOK(_g.notebook), sw2, gtk_label_new(_("Grouped")));
1547
1548 _g.search_list = gtk_list_box_new();
1549 gtk_list_box_set_selection_mode(GTK_LIST_BOX(_g.search_list), GTK_SELECTION_NONE);
1550 GtkWidget *sw4 = gtk_scrolled_window_new(NULL, NULL);
1551 gtk_container_add(GTK_CONTAINER(sw4), _g.search_list);
1552 _g.page_search = gtk_notebook_append_page(GTK_NOTEBOOK(_g.notebook), sw4, gtk_label_new(_("Search")));
1553
1554 // make each tab expand to fill the notebook width
1555 GList *pages = gtk_container_get_children(GTK_CONTAINER(_g.notebook));
1556 for(GList *l = pages; l; l = l->next)
1557 gtk_container_child_set(GTK_CONTAINER(_g.notebook), GTK_WIDGET(l->data), "tab-expand", TRUE,
1558 "tab-fill", TRUE, NULL);
1559 g_list_free(pages);
1560
1561 gtk_widget_show_all(_g.window);
1562 /* The page left open last time, the memory view at first. Selected before the handler is connected:
1563 * appending the first page made it current, which would otherwise have been kept over it. A notebook
1564 * shows a page only once it is visible, hence after show_all. */
1565 gtk_notebook_set_current_page(GTK_NOTEBOOK(_g.notebook),
1567 g_signal_connect(_g.notebook, "switch-page", G_CALLBACK(_on_page_changed), NULL);
1568 _full_reload();
1569 _g.timer_id = g_timeout_add(POLL_INTERVAL_MS, _poll, NULL);
1570}
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
const float top
static const int row
void dt_conf_set_bool(const char *name, int val)
int dt_conf_get_bool(const char *name)
int dt_conf_key_exists(const char *key)
Does key have a value?
void dt_conf_set_int(const char *name, int val)
int dt_conf_get_int(const char *name)
Integer for name, clamped to the bounds declared in the XML.
void dt_conf_set_string(const char *name, const char *val)
const char * dt_conf_get_string_const(const char *name)
Borrow the stored string for name without copying it.
size_t dt_get_total_mem(void)
Definition darktable.c:2422
GHashTable * pages
row label -> path of the illustration, relative to the destination
const char * tooltip
Definition image.h:319
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
#define IS_NULL_PTR(p)
C is way too permissive with !=, == and if(var) checks, which can mean too many things depending on w...
Definition macros.h:96
char * key
void dt_mipmap_cache_get_usage(size_t *current, size_t *max)
uint32_t width
Definition mipmap_cache.c:0
uint32_t height
Definition mipmap_cache.c:1
size_t size
Definition mipmap_cache.c:3
GArray * dt_mipmap_cache_get_entries_stats(void)
const char * dt_opencl_get_device_name(const int devid)
Human-readable device name, owned by the OpenCL module.
Definition opencl.c:2050
size_t dt_opencl_get_device_headroom(const int devid)
Bytes of the device left to other applications (its forced_headroom, from its own config key or memor...
Definition opencl.c:2084
size_t dt_opencl_get_device_memory_in_use(const int devid)
Bytes Ansel holds on the device by its own accounting (dt_opencl_memory_statistics()),...
Definition opencl.c:2076
int dt_opencl_is_enabled(void)
Definition opencl.c:3335
size_t dt_opencl_get_device_max_global_mem(const int devid)
Total device memory in bytes, or 0 for an out-of-range id.
Definition opencl.c:2068
int dt_opencl_get_num_devices(void)
Number of usable OpenCL devices; 0 when OpenCL is unavailable.
Definition opencl.c:2043
const char * name
Definition pdf.h:90
GArray * dt_dev_pixelpipe_cache_get_entries_stats(void)
size_t dt_dev_pixelpipe_cache_get_max_memory(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:1121
GtkWidget * dt_ui_scroll_wrap_get_scrolled_window(GtkWidget *wrapper)
Return the inner scrolled window of a dt_ui_scroll_wrap() wrapper, or NULL.
GtkWidget * dt_ui_scroll_wrap(GtkWidget *w, gint min_size, const char *config_str, dt_ui_resize_mode_t mode)
Wrap a scrollable content widget in a recessed, vertically resizable scrolled window.
@ DT_UI_RESIZE_STATIC
Definition scroll_wrap.h:47
unsigned __int64 uint64_t
Definition strptime.c:75
_bar_segment_t seg[4]
const char * free_label
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 host_stale
int hits
gboolean allocation
dt_pixel_cache_stats_device_t * devices
size_t size
size_t host_bytes
uint64_t hash
int64_t age_us
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)
static void _on_mem_section_expanded(GObject *expander, GParamSpec *pspec, gpointer user_data)
GtkWidget * timeline_list
#define TIMELINE_TRIM_SLACK
static GtkWidget * _add_mem_section(GtkWidget *parent, const char *key, const char *title, const _bar_t *bar)
guint timer_id
static gboolean _on_configure(GtkWidget *window, GdkEventConfigure *event, gpointer user_data)
uint64_t last_seq
static struct @31 _g
static gboolean _poll(gpointer u)
static gint _cmp_device_size(gconstpointer a, gconstpointer b, gpointer data)
static size_t _bar_free(const _bar_t *bar)
static gchar * _bar_segment_text(const _bar_segment_t *seg)
#define SUPERVISOR_CONF_RECORD
static void _wire_hash_label(GtkWidget *label)
static void _group_set_header(_group_t *g)
GtkWidget * timeline_scroll
GtkWidget * window
guint tick
#define BAR_SEGMENT_GAP
static void _run_search(const char *query)
static size_t _bar_held(const _bar_t *bar)
GtkWidget * search_list
gchar * scroll_target
GtkWidget * count_label
static void _bar_segment_span(const _bar_t *bar, const double width, const int k, double *x0, double *x1)
static void _timeline_trim(const guint incoming)
#define SUPERVISOR_CONF_WIDTH
static void _add_device_section(GArray *pe, const int devid)
static void _on_groupby_changed(GtkComboBox *c, gpointer u)
static int _page_from_name(const char *name)
#define SUPERVISOR_CONF_GROUP_BY
GtkWidget * grouped_list
#define COLOR_ALLOCATIONS
static gboolean _window_position_is_usable(GtkWidget *window)
uint64_t grouped_last_seq
#define SUPERVISOR_CONF_X
static void _restore_geometry(GtkWidget *window)
#define TIMELINE_MAX_ROWS
static gboolean _bar_draw(GtkWidget *widget, cairo_t *cr, gpointer user_data)
#define COLOR_PIPELINE
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 _add_bar(GtkWidget *box, const _bar_t *bar)
static gchar * _mem_conf_key(const char *section, const char *setting)
static void _on_search_changed(GtkSearchEntry *e, gpointer u)
static void _links_free(gpointer p)
static GtkWidget * _mem_list(GtkWidget *body, const char *section)
#define MEMORY_LIST_ROWS
static gint _cmp_ram_size(gconstpointer a, gconstpointer b)
static void _group_free(gpointer p)
GHashTable * mem_lists
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
#define SUPERVISOR_CONF_Y
static const char * _page_name(const int page)
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 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 gboolean _rebuild_memory_idle(gpointer user_data)
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)
#define BAR_SEGMENTS
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)
#define COLOR_MIPMAP
static void _rebuild_memory(void)
static size_t _bar_used(const _bar_t *bar)
static gboolean _hex_contains(const uint64_t h, const char *needle)
#define SUPERVISOR_CONF_HEIGHT
static gchar * _age_text(const int64_t us)
static gboolean _bar_tooltip(GtkWidget *widget, gint x, gint y, gboolean keyboard, GtkTooltip *tooltip, gpointer user_data)
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 gboolean _pointer_button_down(void)
static void _scroll_row_into_view(GtkWidget *row)
static gchar * _header_markup(const dt_sv_logged_event_t *ev)
static GtkWidget * _bar_legend_block(const _bar_segment_t *seg, const char *swatch)
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)
static size_t _bar_segment_size(const _bar_segment_t *seg)
#define POLL_INTERVAL_MS
static void _add_mem_item(GtkWidget *box, const char *markup, const char *trailing, const gboolean evictable)
static void _on_clear(GtkButton *b, gpointer u)
static gboolean _scroll_bottom_idle(gpointer u)
GHashTable * group_map
#define SUPERVISOR_CONF_PAGE
static void _grouped_catch_up(void)
typedef double((*spd)(unsigned long int wavelength, double TempK))
#define MAX(a, b)
Definition thinplate.c:29
void dt_gui_add_class(GtkWidget *widget, const gchar *class_name)