Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
src/caches/cache.c
Go to the documentation of this file.
1/*
2 This file is part of darktable,
3 Copyright (C) 2011-2015 johannes hanika.
4 Copyright (C) 2012 Raphael Manfredi.
5 Copyright (C) 2012 Richard Wonka.
6 Copyright (C) 2012-2014, 2016 Tobias Ellinghaus.
7 Copyright (C) 2013-2016 Roman Lebedev.
8 Copyright (C) 2019 Andreas Schneider.
9 Copyright (C) 2019, 2023, 2025 Aurélien PIERRE.
10 Copyright (C) 2020 Heiko Bauke.
11 Copyright (C) 2020-2021 Pascal Obry.
12 Copyright (C) 2021 Ralf Brown.
13 Copyright (C) 2022 Martin Bařinka.
14 Copyright (C) 2024 Alynx Zhou.
15
16 darktable is free software: you can redistribute it and/or modify
17 it under the terms of the GNU General Public License as published by
18 the Free Software Foundation, either version 3 of the License, or
19 (at your option) any later version.
20
21 darktable is distributed in the hope that it will be useful,
22 but WITHOUT ANY WARRANTY; without even the implied warranty of
23 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 GNU General Public License for more details.
25
26 You should have received a copy of the GNU General Public License
27 along with darktable. If not, see <http://www.gnu.org/licenses/>.
28*/
29
30#include "config.h"
31
32#include "caches/cache.h"
33#include "system/dtpthread.h"
34#include "system/macros.h"
35#include "system/mem_alloc.h"
36
37#include <assert.h>
38#include <inttypes.h>
39#include <stdio.h>
40#include <stdlib.h>
41#include <string.h>
42
43// this implements a concurrent LRU cache
44
46 dt_cache_t *cache,
47 size_t entry_size,
48 size_t cost_quota)
49{
50 cache->cost = 0;
51 cache->lru = 0;
52 cache->entry_size = entry_size;
53 cache->cost_quota = cost_quota;
54 dt_pthread_mutex_init(&cache->lock, 0);
55 cache->allocate = 0;
56 cache->allocate_data = 0;
57 cache->cleanup = 0;
58 cache->cleanup_data = 0;
59 cache->hashtable = g_hash_table_new(0, 0);
60}
61
63{
65 for(GList *l = cache->lru; l; l = g_list_next(l))
66 {
68
69 if(cache->cleanup)
70 {
71 assert(entry->data_size);
73
74 cache->cleanup(cache->cleanup_data, entry);
75 }
76 else
77 {
78 dt_free_align(entry->data);
79 entry->data = NULL;
80 }
81
83 g_slice_free1(sizeof(*entry), entry);
84 }
85 g_list_free(cache->lru);
86 cache->lru = NULL;
88}
89
90int32_t dt_cache_contains(dt_cache_t *cache, const uint32_t key)
91{
93 int32_t result = g_hash_table_contains(cache->hashtable, GINT_TO_POINTER(key));
95 return result;
96}
97
99 dt_cache_t *cache,
100 int (*process)(const uint32_t key, const void *data, void *user_data),
101 void *user_data)
102{
105 gpointer key, value;
106
108 while (g_hash_table_iter_next (&iter, &key, &value))
109 {
111 const int err = process(GPOINTER_TO_INT(key), entry->data, user_data);
112 if(err)
113 {
115 return err;
116 }
117 }
119 return 0;
120}
121
122// return read locked bucket, or NULL if it's not already there.
123// never attempt to allocate a new slot.
124dt_cache_entry_t *dt_cache_testget(dt_cache_t *cache, const uint32_t key, char mode)
125{
126 gpointer orig_key, value;
127 gboolean res;
131 if(res)
132 {
134 // lock the cache entry
135 const int result
136 = (mode == 'w') ? dt_pthread_rwlock_trywrlock(&entry->lock) : dt_pthread_rwlock_tryrdlock(&entry->lock);
137 if(result)
138 { // need to give up mutex so other threads have a chance to get in between and
139 // free the lock we're trying to acquire:
141 return 0;
142 }
143 // bubble up in lru list:
144 cache->lru = g_list_remove_link(cache->lru, entry->link);
145 cache->lru = g_list_concat(cache->lru, entry->link);
147
148 if(mode == 'w')
149 {
150 assert(entry->data_size);
152 }
153
154 // WARNING: do *NOT* unpoison here. it must be done by the caller!
155
156 return entry;
157 }
159 return 0;
160}
161
162// if found, the data void* is returned. if not, it is set to be
163// the given *data and a new hash table entry is created, which can be
164// found using the given key later on.
165dt_cache_entry_t *dt_cache_get_with_caller(dt_cache_t *cache, const uint32_t key, char mode, const char *file, int line)
166{
167 gpointer orig_key, value;
168 gboolean res;
169 int result;
170restart:
174 if(res)
175 { // yay, found. read lock and pass on.
177 if(mode == 'w') result = dt_pthread_rwlock_trywrlock_with_caller(&entry->lock, file, line);
178 else result = dt_pthread_rwlock_tryrdlock_with_caller(&entry->lock, file, line);
179 if(result)
180 { // need to give up mutex so other threads have a chance to get in between and
181 // free the lock we're trying to acquire:
183 g_usleep(5);
184 goto restart;
185 }
186 // bubble up in lru list:
187 cache->lru = g_list_remove_link(cache->lru, entry->link);
188 cache->lru = g_list_concat(cache->lru, entry->link);
190
191#ifdef _DEBUG
192 const pthread_t writer = dt_pthread_rwlock_get_writer(&entry->lock);
193 if(mode == 'w')
194 {
196 }
197 else
198 {
199 assert(!pthread_equal(writer, pthread_self()));
200 }
201#endif
202
203 if(mode == 'w')
204 {
205 assert(entry->data_size);
207 }
208
209 // WARNING: do *NOT* unpoison here. it must be done by the caller!
210
211 return entry;
212 }
213
214 // else, not found, need to allocate.
215
216 // first try to clean up.
217 // also wait if we can't free more than the requested fill ratio.
218 if(cache->cost > 0.8f * cache->cost_quota)
219 {
220 // need to roll back all the way to get a consistent lock state:
221 dt_cache_gc(cache, 0.8f);
222 }
223
224 // here dies your 32-bit system:
226 entry->data = 0;
227 entry->data_size = cache->entry_size;
228 entry->cost = 1;
229 entry->link = g_list_append(0, entry);
230 entry->key = key;
231 entry->_lock_demoting = 0;
232
233 if(cache->allocate)
234 cache->allocate(cache->allocate_data, entry);
235 else
236 entry->data = dt_alloc_align(entry->data_size);
237
238 if(IS_NULL_PTR(entry->data))
239 {
240 dt_free(entry);
242 return NULL;
243 }
244
245 int ret = dt_pthread_rwlock_init(&entry->lock, 0);
246 if(ret) fprintf(stderr, "rwlock init: %d\n", ret);
247
249 assert(cache->allocate || entry->data_size);
250
251 assert(entry->data_size);
253
254 // if allocate callback is given, always return a write lock
255 const int write = ((mode == 'w') || cache->allocate);
256
257 // write lock in case the caller requests it:
258 if(write) dt_pthread_rwlock_wrlock_with_caller(&entry->lock, file, line);
259 else dt_pthread_rwlock_rdlock_with_caller(&entry->lock, file, line);
260
261 cache->cost += entry->cost;
262
263 // put at end of lru list (most recently used):
264 cache->lru = g_list_concat(cache->lru, entry->link);
265
267 // WARNING: do *NOT* unpoison here. it must be done by the caller!
268
269 return entry;
270}
271
272int dt_cache_remove(dt_cache_t *cache, const uint32_t key)
273{
274 gpointer orig_key, value;
275 gboolean res;
276 int result;
277 dt_cache_entry_t *entry;
278restart:
280
283 entry = (dt_cache_entry_t *)value;
284 if(!res)
285 { // not found in cache, not deleting.
287 return 1;
288 }
289 // need write lock to be able to delete:
290 result = dt_pthread_rwlock_trywrlock(&entry->lock);
291 if(result)
292 {
294 g_usleep(5);
295 goto restart;
296 }
297
298 if(entry->_lock_demoting)
299 {
300 // oops, we are currently demoting (rw -> r) lock to this entry in some thread. do not touch!
303 g_usleep(5);
304 goto restart;
305 }
306
308 (void)removed; // make non-assert compile happy
310 cache->lru = g_list_delete_link(cache->lru, entry->link);
311
312 if(cache->cleanup)
313 {
314 assert(entry->data_size);
316
317 cache->cleanup(cache->cleanup_data, entry);
318 }
319 else
320 {
321 dt_free_align(entry->data);
322 entry->data = NULL;
323 }
324
327 cache->cost -= entry->cost;
328 g_slice_free1(sizeof(*entry), entry);
329
331 return 0;
332}
333
334// best-effort garbage collection. never blocks, never fails. well, sometimes it just doesn't free anything.
335void dt_cache_gc(dt_cache_t *cache, const float fill_ratio)
336{
337 GList *l = cache->lru;
338 while(l)
339 {
341 assert(entry->link->data == entry);
342 l = g_list_next(l); // we might remove this element, so walk to the next one while we still have the pointer..
343 if(cache->cost < cache->cost_quota * fill_ratio) break;
344
345 // if still locked by anyone else give up:
346 if(dt_pthread_rwlock_trywrlock(&entry->lock)) continue;
347
348 if(entry->_lock_demoting)
349 {
350 // oops, we are currently demoting (rw -> r) lock to this entry in some thread. do not touch!
352 continue;
353 }
354
355 // delete!
357 cache->lru = g_list_delete_link(cache->lru, entry->link);
358 cache->cost -= entry->cost;
359
360 if(cache->cleanup)
361 {
362 assert(entry->data_size);
364
365 cache->cleanup(cache->cleanup_data, entry);
366 }
367 else
368 {
369 dt_free_align(entry->data);
370 entry->data = NULL;
371 }
372
375 g_slice_free1(sizeof(*entry), entry);
376 }
377}
378
379void dt_cache_release_with_caller(dt_cache_t *cache, dt_cache_entry_t *entry, const char *file, int line)
380{
381#if((__has_feature(address_sanitizer) || defined(__SANITIZE_ADDRESS__)) && 1)
382 /* Yes, this is *HIGHLY* unportable and is accessing implementation details.
383 *
384 * There used to be an #ifdef _DEBUG split here, because dt_pthread_rwlock_t was a bare
385 * pthread_rwlock_t in release builds and a struct wrapping one in debug builds. It is the
386 * struct in both since the same-thread recursive-writer tracking was ported into the
387 * release path, so the non-_DEBUG arm stopped compiling -- and nobody found out, because
388 * this whole block only exists under AddressSanitizer and nothing in CI builds with it.
389 * An ASAN build of this tree has been failing here ever since. One arm now. */
390# if defined(HAVE_THREAD_RWLOCK_ARCH_T_READERS)
391 if(entry->lock.lock.__data.__readers <= 1)
392# elif defined(HAVE_THREAD_RWLOCK_ARCH_T_NR_READERS)
393 if(entry->lock.lock.__data.__nr_readers <= 1)
394# else /* HAVE_THREAD_RWLOCK_ARCH_T_(NR_)READERS */
395# error "No valid reader member"
396# endif /* HAVE_THREAD_RWLOCK_ARCH_T_(NR_)READERS */
397 {
398 // only if there are no other reades we may poison.
399 assert(entry->data_size);
401 }
402#endif
403
405}
406
407int dt_cache_seed(dt_cache_t *cache, const uint32_t key, const void *data, size_t data_size, size_t cost,
408 gboolean aligned_alloc)
409{
410 if(IS_NULL_PTR(cache) || IS_NULL_PTR(data) || data_size == 0) return -1;
411
414 {
416 return 1;
417 }
418
419 if(cache->cost > 0.8f * cache->cost_quota)
420 dt_cache_gc(cache, 0.8f);
421
423 entry->data = 0;
424 entry->data_size = data_size;
425 entry->cost = cost ? cost : data_size;
426 entry->link = g_list_append(0, entry);
427 entry->key = key;
428 entry->_lock_demoting = 0;
429
430 entry->data = aligned_alloc ? dt_alloc_align(entry->data_size) : g_malloc(entry->data_size);
431 if(IS_NULL_PTR(entry->data))
432 {
433 g_slice_free1(sizeof(*entry), entry);
435 return -1;
436 }
437
438 memcpy(entry->data, data, entry->data_size);
439
440 int ret = dt_pthread_rwlock_init(&entry->lock, 0);
441 if(ret) fprintf(stderr, "rwlock init: %d\n", ret);
442
444 cache->lru = g_list_concat(cache->lru, entry->link);
445 cache->cost += entry->cost;
446
447 assert(entry->data_size);
449
451 return 0;
452}
453
458
460{
461 if(IS_NULL_PTR(entry)) return;
462 dt_free_align(entry->data);
463 free(entry);
464}
465
466// clang-format off
467// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
468// vim: shiftwidth=2 expandtab tabstop=2 cindent
469// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
470// clang-format on
__DT_CLONE_TARGETS__ int process(struct dt_iop_module_t *self, const dt_dev_pixelpipe_t *pipe, const dt_dev_pixelpipe_iop_t *piece, const void *const ivoid, void *const ovoid)
Definition ashift.c:3166
typedef void((*dt_cache_allocate_t)(void *userdata, dt_cache_entry_t *entry))
#define ASAN_POISON_MEMORY_REGION(addr, size)
#define ASAN_UNPOISON_MEMORY_REGION(addr, size)
void * dt_alloc_align(size_t size)
Definition darktable.c:508
static int dt_pthread_rwlock_unlock(dt_pthread_rwlock_t *rwlock)
Definition dtpthread.h:453
static int dt_pthread_rwlock_tryrdlock(dt_pthread_rwlock_t *rwlock)
Definition dtpthread.h:575
#define dt_pthread_rwlock_wrlock_with_caller(A, B, C)
Definition dtpthread.h:597
#define dt_pthread_rwlock_trywrlock_with_caller(A, B, C)
Definition dtpthread.h:599
#define dt_pthread_rwlock_tryrdlock_with_caller(A, B, C)
Definition dtpthread.h:598
static int dt_pthread_mutex_unlock(dt_pthread_mutex_t *mutex) RELEASE(mutex) NO_THREAD_SAFETY_ANALYSIS
Definition dtpthread.h:385
static int dt_pthread_rwlock_trywrlock(dt_pthread_rwlock_t *rwlock)
Definition dtpthread.h:584
static int dt_pthread_mutex_init(dt_pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
Definition dtpthread.h:370
static int dt_pthread_rwlock_destroy(dt_pthread_rwlock_t *lock)
Definition dtpthread.h:448
static int dt_pthread_mutex_destroy(dt_pthread_mutex_t *mutex)
Definition dtpthread.h:390
static int dt_pthread_rwlock_init(dt_pthread_rwlock_t *lock, const pthread_rwlockattr_t *attr)
Definition dtpthread.h:428
static int dt_pthread_mutex_lock(dt_pthread_mutex_t *mutex) ACQUIRE(mutex) NO_THREAD_SAFETY_ANALYSIS
Definition dtpthread.h:375
#define dt_pthread_rwlock_rdlock_with_caller(A, B, C)
Definition dtpthread.h:596
#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:65
#define dt_free_align(ptr)
Definition mem_alloc.h:122
#define dt_free(ptr)
Definition mem_alloc.h:97
char * key
static const dt_aligned_pixel_simd_t value
Definition simd.h:144
int32_t dt_cache_contains(dt_cache_t *cache, const uint32_t key)
int dt_cache_seed(dt_cache_t *cache, const uint32_t key, const void *data, size_t data_size, size_t cost, gboolean aligned_alloc)
void dt_cache_entry_free_detached(dt_cache_entry_t *entry)
Release an entry from dt_cache_entry_new_detached(), and its data. NULL-safe.
void dt_cache_gc(dt_cache_t *cache, const float fill_ratio)
void dt_cache_release_with_caller(dt_cache_t *cache, dt_cache_entry_t *entry, const char *file, int line)
dt_cache_entry_t * dt_cache_testget(dt_cache_t *cache, const uint32_t key, char mode)
dt_cache_entry_t * dt_cache_entry_new_detached(void)
Allocate a cache entry that belongs to NO cache, for a one-shot decode.
void dt_cache_init(dt_cache_t *cache, size_t entry_size, size_t cost_quota)
dt_cache_entry_t * dt_cache_get_with_caller(dt_cache_t *cache, const uint32_t key, char mode, const char *file, int line)
int dt_cache_remove(dt_cache_t *cache, const uint32_t key)
int dt_cache_for_all(dt_cache_t *cache, int(*process)(const uint32_t key, const void *data, void *user_data), void *user_data)
void dt_cache_cleanup(dt_cache_t *cache)
uint32_t key
GList * link
size_t data_size
dt_pthread_rwlock_t lock
int _lock_demoting
void * data
size_t cost
size_t cost
GList * lru
size_t cost_quota
void * allocate_data
GHashTable * hashtable
dt_cache_allocate_t allocate
dt_pthread_mutex_t lock
size_t entry_size
dt_cache_allocate_t cleanup
void * cleanup_data
pthread_rwlock_t lock
Definition dtpthread.h:409