Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
memory_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
20#include "system/macros.h"
21
22#include <glib.h>
23#include <glib/gstdio.h>
24#include <inttypes.h>
25#include <stdio.h>
26#include <string.h>
27
28#if defined(__linux__)
29#include <errno.h> // conditional-ok: only the watcher's poll() below reports EINTR
30#include <pthread.h> // conditional-ok: idem -- the thread the watcher sleeps on
31#include <fcntl.h> // conditional-ok: idem -- every PSI file is opened inside this same #ifdef
32#include <poll.h> // conditional-ok: idem -- the watcher sleeps on those descriptors
33#include <sys/eventfd.h> // conditional-ok: idem -- the eventfd that wakes it to stop
34#include <unistd.h> // conditional-ok: idem -- read(), write() and close() on those descriptors
35
36// The `total` of the "full" line of one PSI file. FALSE when the file is absent (a kernel
37// without PSI, a cgroup level without the memory controller) or unreadable.
38static gboolean _read_full_total(const char *file, uint64_t *total_us)
39{
40 FILE *f = g_fopen(file, "r");
41 if(IS_NULL_PTR(f)) return FALSE;
42
43 gboolean found = FALSE;
44 char line[256];
45 while(!found && fgets(line, sizeof(line), f))
46 {
47 unsigned long long total = 0;
48 if(sscanf(line, "full avg10=%*f avg60=%*f avg300=%*f total=%llu", &total) == 1)
49 {
50 *total_us = (uint64_t)total;
51 found = TRUE;
52 }
53 }
54 fclose(f);
55 return found;
56}
57
58// The process's cgroup v2 path, "/user.slice/...", or an empty string when it has none.
59static void _own_cgroup_path(char *path, const size_t size)
60{
61 path[0] = '\0';
62 FILE *f = g_fopen("/proc/self/cgroup", "r");
63 if(IS_NULL_PTR(f)) return;
64
65 char line[512];
66 while(fgets(line, sizeof(line), f))
67 {
68 // cgroup v2 unified hierarchy entry: "0::/user.slice/..."
69 if(!strncmp(line, "0::", 3))
70 {
71 g_strlcpy(path, line + 3, size);
72 char *newline = strchr(path, '\n');
73 if(newline) *newline = '\0';
74 break;
75 }
76 }
77 fclose(f);
78}
79
80// Whether a cgroup path names a level below the root.
81static gboolean _cgroup_below_root(const char *path)
82{
83 return path[0] == '/' && path[1] != '\0';
84}
85
86// Moves a cgroup path to its parent, in place. FALSE once it was a top-level cgroup: the root has
87// no memory.pressure, and the system-wide file stands for it.
88static gboolean _cgroup_parent(char *path)
89{
90 char *slash = strrchr(path, '/');
91 if(IS_NULL_PTR(slash) || slash == path) return FALSE;
92 *slash = '\0';
93 return TRUE;
94}
95
96static int _open_trigger(const char *file, const char *trigger)
97{
98 const int fd = open(file, O_RDWR | O_NONBLOCK | O_CLOEXEC);
99 if(fd < 0) return -1;
100
101 // The kernel reads the trigger up to its terminating NUL, which is written too.
102 if(write(fd, trigger, strlen(trigger) + 1) < 0)
103 {
104 close(fd);
105 return -1;
106 }
107 return fd;
108}
109#endif
110
112{
113#if defined(__linux__)
114 if(IS_NULL_PTR(total_us) || max_levels <= 0) return 0;
115
116 int levels = 0;
117 if(!_read_full_total("/proc/pressure/memory", &total_us[levels])) return 0;
118 levels++;
119
120 // A level whose file is missing stays missing, so skipping it keeps the order stable from one
121 // read to the next.
122 char path[512];
123 _own_cgroup_path(path, sizeof(path));
124 gboolean more = _cgroup_below_root(path);
125 while(more && levels < max_levels)
126 {
127 char file[600];
128 snprintf(file, sizeof(file), "/sys/fs/cgroup%s/memory.pressure", path);
129 if(_read_full_total(file, &total_us[levels])) levels++;
130 more = _cgroup_parent(path);
131 }
132 return levels;
133#else
134 (void)total_us;
136 return 0;
137#endif
138}
139
140#if defined(__linux__)
141/* Arm a trigger -- `stall_us` of full stall within any `window_us` -- on every level that accepts
142 * one. Writes up to `max_fds` descriptors to `fds` and returns how many. Levels this process may
143 * not write to (root-owned ones) simply refuse it. */
144static int _triggers_open(int *fds, int max_fds, uint64_t stall_us, uint64_t window_us)
145{
146 char trigger[64];
147 snprintf(trigger, sizeof(trigger), "full %" PRIu64 " %" PRIu64, stall_us, window_us);
148
149 int count = 0;
150 const int system_fd = _open_trigger("/proc/pressure/memory", trigger);
151 if(system_fd >= 0) fds[count++] = system_fd;
152
153 char path[512];
154 _own_cgroup_path(path, sizeof(path));
155 gboolean more = _cgroup_below_root(path);
156 while(more && count < max_fds)
157 {
158 char file[600];
159 snprintf(file, sizeof(file), "/sys/fs/cgroup%s/memory.pressure", path);
160 const int fd = _open_trigger(file, trigger);
161 if(fd >= 0) fds[count++] = fd;
162 more = _cgroup_parent(path);
163 }
164 return count;
165}
166
168{
169 /* Written before the thread starts and after it has joined, read by the thread in between. */
170 pthread_t thread;
172 int count;
174 gboolean running;
175 void (*stalled)(void *user);
176 void *user;
177};
178
180{
181 if(watch->stop_fd >= 0) close(watch->stop_fd);
182 for(int i = 0; i < watch->count; i++) close(watch->fds[i]);
183 g_free(watch);
184}
185
186static void *_watch_thread(void *arg)
187{
189 struct pollfd pfd[DT_MEMORY_PRESSURE_MAX_LEVELS + 1];
190 int n = 0;
191 for(int i = 0; i < watch->count; i++)
192 {
193 pfd[n].fd = watch->fds[i];
194 pfd[n].events = POLLPRI;
195 pfd[n].revents = 0;
196 n++;
197 }
198 const int stop = n;
199 pfd[n].fd = watch->stop_fd;
200 pfd[n].events = POLLIN;
201 pfd[n].revents = 0;
202 n++;
203
204 while(TRUE)
205 {
206 if(poll(pfd, n, -1) < 0)
207 {
208 if(errno == EINTR) continue;
209 break;
210 }
211 if(pfd[stop].revents) break;
212
213 gboolean fired = FALSE;
214 for(int i = 0; i < stop; i++)
215 {
216 if(pfd[i].revents & POLLPRI) fired = TRUE;
217 // That level is gone (its cgroup was removed): poll() skips a negative descriptor, and
218 // the stop path still closes the one we opened.
219 if(pfd[i].revents & (POLLERR | POLLNVAL)) pfd[i].fd = -1;
220 }
221 if(fired) watch->stalled(watch->user);
222 }
223 return NULL;
224}
225#endif
226
228 void (*stalled)(void *user), void *user)
229{
230#if defined(__linux__)
231 if(IS_NULL_PTR(stalled)) return NULL;
232
233 dt_memory_pressure_watch_t *watch = g_malloc0(sizeof(dt_memory_pressure_watch_t));
234 watch->stop_fd = -1;
235 watch->stalled = stalled;
236 watch->user = user;
237
238 watch->count = _triggers_open(watch->fds, DT_MEMORY_PRESSURE_MAX_LEVELS, stall_us, window_us);
239 if(watch->count == 0)
240 {
241 _watch_close(watch);
242 return NULL;
243 }
244
245 /* pthread_create() rather than dt_pthread_create(): that one reads a conf key to set the
246 * thread's FP mode, and src/system holds no state and reaches none. This thread sleeps in
247 * poll() and calls back; it does no arithmetic of its own, and the default stack is ample. */
248 watch->stop_fd = eventfd(0, EFD_CLOEXEC);
249 if(watch->stop_fd < 0 || pthread_create(&watch->thread, NULL, _watch_thread, watch) != 0)
250 {
251 _watch_close(watch);
252 return NULL;
253 }
254
255 watch->running = TRUE;
256 return watch;
257#else
258 (void)stall_us;
259 (void)window_us;
260 (void)stalled;
261 (void)user;
262 return NULL;
263#endif
264}
265
267{
268 if(IS_NULL_PTR(watch) || IS_NULL_PTR(*watch)) return;
269
270#if defined(__linux__)
271 dt_memory_pressure_watch_t *w = *watch;
272 if(w->running)
273 {
274 const uint64_t wake = 1;
275 if(write(w->stop_fd, &wake, sizeof(wake)) != (ssize_t)sizeof(wake))
276 pthread_cancel(w->thread); // poll() is a cancellation point
277 pthread_join(w->thread, NULL);
278 }
279 _watch_close(w);
280#endif
281
282 // Where the platform arms nothing, the handle was never non-NULL and this is unreachable.
283 *watch = NULL;
284}
285
287{
288#if defined(__linux__)
289 return IS_NULL_PTR(watch) ? 0 : watch->count;
290#else
291 (void)watch;
292 return 0;
293#endif
294}
295
296// clang-format off
297// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
298// vim: shiftwidth=2 expandtab tabstop=2 cindent
299// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
300// clang-format on
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
int levels(struct dt_imageio_module_data_t *data)
Definition avif.c:641
typedef void((*dt_cache_allocate_t)(void *userdata, dt_cache_entry_t *entry))
const float f
#define max_levels
#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
static gboolean _read_full_total(const char *file, uint64_t *total_us)
int dt_memory_pressure_watch_levels(const dt_memory_pressure_watch_t *watch)
static int _triggers_open(int *fds, int max_fds, uint64_t stall_us, uint64_t window_us)
static void _watch_close(dt_memory_pressure_watch_t *watch)
static gboolean _cgroup_parent(char *path)
static void _own_cgroup_path(char *path, const size_t size)
static gboolean _cgroup_below_root(const char *path)
int dt_memory_pressure_read_full_stall(uint64_t *total_us, int max_levels)
static int _open_trigger(const char *file, const char *trigger)
void dt_memory_pressure_watch_stop(dt_memory_pressure_watch_t **watch)
static void * _watch_thread(void *arg)
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
size_t size
Definition mipmap_cache.c:3
unsigned __int64 uint64_t
Definition strptime.c:75
void(* stalled)(void *user)
int fds[DT_MEMORY_PRESSURE_MAX_LEVELS]