Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
pixelpipe_cache_pressure.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
19/* When the pixelpipe cache gives memory back, and how much.
20 *
21 * The cache budget is a plan made at startup, and the available-RAM valve in pixelpipe_cache.c
22 * guards a floor of free memory. Neither sees a system that still reports memory available but
23 * spends its time reclaiming it: swap full, other applications' pages evicted and faulted straight
24 * back in. That stall is what systemd-oomd watches -- on an Ubuntu 24.04 user session, past 50%
25 * "full" stall of user@.service for 20 s it kills the cgroup under it reclaiming the most, which
26 * on a photo workstation is us -- while MemAvailable is still far above any floor.
27 *
28 * So every DT_PIXELPIPE_CACHE_PSI_WINDOW_US, measure the share of the window during which every
29 * task was stalled on memory, the highest over the system and each cgroup above us. Past
30 * DT_PIXELPIPE_CACHE_PSI_SHED, shed a quarter of the cache (half past three times that), hand the
31 * pages to the OS, and lower the budget to what is left, so the pipes do not fill it straight
32 * back up; the next window says whether that was enough.
33 *
34 * That measurement runs where the cache is already doing something -- an allocation, the idle
35 * shedder's timer -- and neither happens once the stall is severe: a machine thrashing stops the
36 * GUI thread's timers and the pipelines alike. Measured: a run went from calm to dead with the
37 * last 32 seconds silent, the process frozen with 13.6 GB resident while oomd counted its 20
38 * seconds. So the reaction does not wait for the cache to run: a watcher sleeps on the kernel's
39 * own PSI triggers (dt_memory_pressure_watch_start()), which the kernel raises as soon as a window
40 * is stalled past the same threshold, and sheds from there. It calls back on its own thread, which
41 * is why every function here is documented as running under the cache's lock: the cache takes it
42 * before calling in, from that thread like from any other.
43 *
44 * Once windows run calm, the budget climbs back by 1/32 of the plan per window, but only to 7/8 of
45 * the footprint the cache had when pressure struck; that mark itself rises by 1/1024 of the plan
46 * per calm window, so the size that caused the stall is tried again only after minutes of calm.
47 * Climbing straight back to the plan brought the stall back within half a minute, every time, and
48 * once to 49%, one point under systemd-oomd's limit.
49 *
50 * The share comes from PSI's cumulative `total` counters, not from its avg10: that is a 10 s
51 * moving average, which keeps reading high for some 20 s after a stall has ended, and shedding on
52 * it would drain the whole cache for pressure that was already gone.
53 *
54 * The arithmetic of the ceiling and the mark is in the header, where a test can reach it. */
55
57
58#include "common/logging.h"
59#include "system/macros.h"
60
61#include <string.h>
62
64{
65 memset(m->totals, 0, sizeof(m->totals));
66 m->levels = 0;
67 m->time_us = 0;
68 m->shed_time_us = 0;
69 m->watch = NULL;
70 dt_pixelpipe_cache_pressure_init(&m->budget, plan);
71}
72
73/* Full-stall share of the window since the previous read, the highest over the levels
74 * dt_memory_pressure_read_full_stall() reports. -1 before `min_window_us` has elapsed, on the
75 * first read, and where the platform has no PSI. The kernel-woken path passes a shorter
76 * `min_window_us` than the polling ones: it already knows a window was stalled, and only reads
77 * the counters to tell a bad stall from a catastrophic one. WARNING: non thread-safe */
78static double _window_share(dt_pixelpipe_cache_pressure_monitor_t *m, const gint64 now,
79 const gint64 min_window_us)
80{
81 if(m->time_us != 0 && now - m->time_us < min_window_us) return -1.;
82
85 const gint64 elapsed = now - m->time_us;
86
87 double share = -1.;
88 if(m->time_us != 0 && levels > 0 && levels == m->levels)
89 {
90 share = 0.;
91 for(int i = 0; i < levels; i++)
92 if(totals[i] >= m->totals[i])
93 share = MAX(share, (double)(totals[i] - m->totals[i]) / (double)elapsed);
94 }
95
96 memcpy(m->totals, totals, sizeof(m->totals));
97 m->levels = levels;
98 m->time_us = now;
99 return share;
100}
101
102// Give memory back for a window `share` of which was fully stalled. Returns the bytes shed.
103// WARNING: non thread-safe
105 const dt_pixelpipe_cache_pressure_sink_t *sink, const double share, const char *origin)
106{
107 const size_t before = sink->held(sink->user);
108 const size_t target = dt_pixelpipe_cache_pressure_shed_target(&m->budget, share, before);
109 if(target == DT_PIXELPIPE_CACHE_PRESSURE_KEEP) return 0;
110
111 size_t given_back = 0;
112 const size_t after = sink->shed(sink->user, target, &given_back);
113
114 dt_pixelpipe_cache_pressure_after_shed(&m->budget, before, after);
115 m->shed_time_us = g_get_monotonic_time();
116
117 const size_t freed = before - after;
119 "[pixelpipe_cache] kernel memory pressure (%s): %.0f%% of the window stalled -- shed %" G_GSIZE_FORMAT
120 " MiB of cache, returned %" G_GSIZE_FORMAT " MiB to the OS, budget lowered to %" G_GSIZE_FORMAT " MiB\n",
121 origin, 100. * share, freed / (1024 * 1024), given_back / (1024 * 1024),
122 dt_pixelpipe_cache_pressure_budget(&m->budget) / (1024 * 1024));
123 return freed;
124}
125
126// Two paths shed: the kernel's wake-up and the windows measured here. Neither needs to repeat what
127// the other just did. WARNING: non thread-safe
128static inline gboolean _shed_too_soon(const dt_pixelpipe_cache_pressure_monitor_t *m, const gint64 now)
129{
130 return m->shed_time_us != 0 && now - m->shed_time_us < DT_PIXELPIPE_CACHE_PSI_WINDOW_US / 2;
131}
132
135{
136 const gint64 now = g_get_monotonic_time();
137 const double share = _window_share(m, now, DT_PIXELPIPE_CACHE_PSI_WINDOW_US);
138 if(share < 0.) return 0;
139
140 if(share >= DT_PIXELPIPE_CACHE_PSI_SHED)
141 return _shed_too_soon(m, now) ? 0 : _shed(m, sink, share, "measured");
142
145 "[pixelpipe_cache] kernel memory pressure calm: budget raised to %" G_GSIZE_FORMAT
146 " MiB (at most %" G_GSIZE_FORMAT " MiB until the pressure mark recovers)\n",
147 dt_pixelpipe_cache_pressure_budget(&m->budget) / (1024 * 1024),
148 dt_pixelpipe_cache_pressure_cap(&m->budget) / (1024 * 1024));
149 return 0;
150}
151
152/* The kernel raised a trigger: a window was stalled past DT_PIXELPIPE_CACHE_PSI_SHED, so shed
153 * without waiting for a window of our own to close. The counters still say HOW stalled it was --
154 * over whatever has elapsed since the last read, which the trigger guarantees is recent -- and
155 * that is what picks a quarter or a half; when the last read is too close to tell, the threshold
156 * the trigger fired at stands in. */
159{
160 const gint64 now = g_get_monotonic_time();
161 if(_shed_too_soon(m, now)) return;
162
163 const double measured = _window_share(m, now, DT_PIXELPIPE_CACHE_PSI_WINDOW_US / 8);
164 _shed(m, sink, MAX(measured, DT_PIXELPIPE_CACHE_PSI_SHED), "kernel wake-up");
165}
166
168 void (*wake)(void *user), void *user)
169{
173
174 // No watcher: the measured windows still react, just not while nothing of ours runs.
175 if(!IS_NULL_PTR(m->watch))
177 "[pixelpipe_cache] watching kernel memory pressure on %i level(s)\n",
179}
180
185
186// clang-format off
187// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
188// vim: shiftwidth=2 expandtab tabstop=2 cindent
189// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
190// clang-format on
int levels(struct dt_imageio_module_data_t *data)
Definition avif.c:641
#define m
Definition basecurve.c:283
@ DT_DEBUG_PIPECACHE
Definition logging.h:56
@ DT_DEBUG_MEMORY
Definition logging.h:59
void dt_print(dt_debug_thread_t thread, const char *msg,...) __attribute__((format(printf
Print to stdout when thread is enabled, prefixed with seconds since startup.
#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
int dt_memory_pressure_watch_levels(const dt_memory_pressure_watch_t *watch)
int dt_memory_pressure_read_full_stall(uint64_t *total_us, int max_levels)
void dt_memory_pressure_watch_stop(dt_memory_pressure_watch_t **watch)
dt_memory_pressure_watch_t * dt_memory_pressure_watch_start(uint64_t stall_us, uint64_t window_us, void(*stalled)(void *user), void *user)
#define DT_MEMORY_PRESSURE_MAX_LEVELS
static double _window_share(dt_pixelpipe_cache_pressure_monitor_t *m, const gint64 now, const gint64 min_window_us)
void dt_pixelpipe_cache_pressure_monitor_init(dt_pixelpipe_cache_pressure_monitor_t *m, const size_t plan)
void dt_pixelpipe_cache_pressure_triggered(dt_pixelpipe_cache_pressure_monitor_t *m, const dt_pixelpipe_cache_pressure_sink_t *sink)
static gboolean _shed_too_soon(const dt_pixelpipe_cache_pressure_monitor_t *m, const gint64 now)
static size_t _shed(dt_pixelpipe_cache_pressure_monitor_t *m, const dt_pixelpipe_cache_pressure_sink_t *sink, const double share, const char *origin)
void dt_pixelpipe_cache_pressure_watch_start(dt_pixelpipe_cache_pressure_monitor_t *m, void(*wake)(void *user), void *user)
size_t dt_pixelpipe_cache_pressure_react(dt_pixelpipe_cache_pressure_monitor_t *m, const dt_pixelpipe_cache_pressure_sink_t *sink)
void dt_pixelpipe_cache_pressure_watch_stop(dt_pixelpipe_cache_pressure_monitor_t *m)
#define DT_PIXELPIPE_CACHE_PRESSURE_KEEP
static size_t dt_pixelpipe_cache_pressure_cap(const dt_pixelpipe_cache_pressure_t *p)
static size_t dt_pixelpipe_cache_pressure_shed_target(const dt_pixelpipe_cache_pressure_t *p, const double share, const size_t current)
#define DT_PIXELPIPE_CACHE_PSI_SHED
static void dt_pixelpipe_cache_pressure_after_shed(dt_pixelpipe_cache_pressure_t *p, const size_t before, const size_t after)
static gboolean dt_pixelpipe_cache_pressure_relax(dt_pixelpipe_cache_pressure_t *p)
static void dt_pixelpipe_cache_pressure_init(dt_pixelpipe_cache_pressure_t *p, const size_t plan)
#define DT_PIXELPIPE_CACHE_PSI_CALM
#define DT_PIXELPIPE_CACHE_PSI_WINDOW_US
static size_t dt_pixelpipe_cache_pressure_budget(const dt_pixelpipe_cache_pressure_t *p)
unsigned __int64 uint64_t
Definition strptime.c:75
size_t(* shed)(void *user, size_t target, size_t *given_back)
#define MAX(a, b)
Definition thinplate.c:29