Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
test_history_snapshot.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/* The contract between a history snapshot and the writer's copy-on-write gate.
20 *
21 * dt_dev_pixelpipe_change() resyncs a pipe against a dt_dev_history_snapshot_t with
22 * history_mutex RELEASED. That is only sound if two things hold, and this file pins both:
23 *
24 * 1. the snapshot holds one reference per item, so nothing it walks can be freed under it;
25 * 2. dt_dev_history_cow_touch() clones a shared item instead of mutating it in place, so
26 * nothing the snapshot walks can CHANGE under it either -- and the pipe's last-synced
27 * marker, which the GUI thread re-points at the clone, keeps every refcount balanced.
28 *
29 * A dt_develop_t is built by hand here: the functions under test touch dev->history,
30 * dev->history_mutex, dev->pipe and dev->preview_pipe and nothing else, and dt_dev_init()
31 * would drag the whole application in for no gain. */
32
33#include "develop/develop.h"
34#include "develop/dev_history.h"
36#include "system/atomic.h"
37#include "system/dtpthread.h"
38
39#include <glib.h>
40#include <stdarg.h>
41#include <stddef.h>
42#include <stdlib.h>
43// cmocka.h declares `extern jmp_buf global_expect_assert_env' at file scope without including
44// <setjmp.h> itself. Same suppression as the other tests here, and for the same reason.
45#include <setjmp.h> // NOLINT(misc-include-cleaner)
46#include <stdint.h>
47#include <cmocka.h>
48
54
55static int _setup(void **state)
56{
57 _fixture_t *f = calloc(1, sizeof(_fixture_t));
58 if(!f) return -1;
59 dt_pthread_rwlock_init(&f->dev.history_mutex, NULL);
60 f->dev.pipe = &f->pipe;
61 f->dev.preview_pipe = NULL;
62 *state = f;
63 return 0;
64}
65
66static int _teardown(void **state)
67{
68 _fixture_t *f = *state;
69 // Whatever a test left in the pipe's marker and in history is released here, so a test
70 // that asserts on refcounts mid-way does not have to also be its own janitor.
71 dt_dev_free_history_item(dt_atomic_exch_ptr(&f->pipe.last_history_item, NULL));
72 g_list_free_full(f->dev.history, dt_dev_free_history_item);
73 f->dev.history = NULL;
74 dt_pthread_rwlock_destroy(&f->dev.history_mutex);
75 free(f);
76 return 0;
77}
78
79static int _refs(const dt_dev_history_item_t *item)
80{
81 return dt_atomic_get_int(&((dt_dev_history_item_t *)item)->refcount);
82}
83
85{
87 assert_non_null(item);
88 item->hash = hash;
89 dev->history = g_list_append(dev->history, item);
90 item->num = g_list_index(dev->history, item);
91 return item;
92}
93
95{
96 _fixture_t *f = *state;
97 dt_dev_history_item_t *a = _append(&f->dev, 0xA);
98 dt_dev_history_item_t *b = _append(&f->dev, 0xB);
99 dt_dev_history_item_t *c = _append(&f->dev, 0xC);
100 f->dev.history_end = 3;
101 dt_dev_set_history_hash(&f->dev, 0xABC);
102
104 dt_pthread_rwlock_rdlock(&f->dev.history_mutex);
105 dt_dev_history_snapshot_take(&f->dev, &snap);
106 dt_pthread_rwlock_unlock(&f->dev.history_mutex);
107
108 // Same items, same order, one extra reference each. No copy: the pointers are identical.
109 assert_int_equal(g_list_length(snap.items), 3);
110 assert_ptr_equal(g_list_nth_data(snap.items, 0), a);
111 assert_ptr_equal(g_list_nth_data(snap.items, 1), b);
112 assert_ptr_equal(g_list_nth_data(snap.items, 2), c);
113 assert_int_equal(_refs(a), 2);
114 assert_int_equal(_refs(b), 2);
115 assert_int_equal(_refs(c), 2);
116
117 // end and hash travel with the list.
118 assert_int_equal(snap.history_end, 3);
119 assert_int_equal(snap.history_hash, 0xABC);
120
122 assert_null(snap.items);
123 assert_int_equal(_refs(a), 1);
124 assert_int_equal(_refs(b), 1);
125 assert_int_equal(_refs(c), 1);
126}
127
129{
130 _fixture_t *f = *state;
131 _append(&f->dev, 0x1);
132 _append(&f->dev, 0x2);
133 f->dev.history_end = 99; // stale, larger than the list
134
136 dt_pthread_rwlock_rdlock(&f->dev.history_mutex);
137 dt_dev_history_snapshot_take(&f->dev, &snap);
138 dt_pthread_rwlock_unlock(&f->dev.history_mutex);
139
140 assert_int_equal(snap.history_end, 2);
142}
143
145{
146 _fixture_t *f = *state;
147 dt_dev_history_item_t *item = _append(&f->dev, 0x1);
148 assert_int_equal(_refs(item), 1);
149
150 dt_dev_history_item_t *touched = dt_dev_history_cow_touch(&f->dev, item);
151
152 // Nobody else can see it: mutate in place, no clone, no churn.
153 assert_ptr_equal(touched, item);
154 assert_int_equal(_refs(item), 1);
155 assert_ptr_equal(g_list_nth_data(f->dev.history, 0), item);
156}
157
159{
160 _fixture_t *f = *state;
161 dt_dev_history_item_t *original = _append(&f->dev, 0x1);
162 original->enabled = TRUE;
163 f->dev.history_end = 1;
164
166 dt_pthread_rwlock_rdlock(&f->dev.history_mutex);
167 dt_dev_history_snapshot_take(&f->dev, &snap);
168 dt_pthread_rwlock_unlock(&f->dev.history_mutex);
169 assert_int_equal(_refs(original), 2);
170
171 dt_dev_history_item_t *clone = dt_dev_history_cow_touch(&f->dev, original);
172
173 // The writer got a fresh object and dev->history now names it ...
174 assert_non_null(clone);
175 assert_ptr_not_equal(clone, original);
176 assert_ptr_equal(g_list_nth_data(f->dev.history, 0), clone);
177 assert_int_equal(_refs(clone), 1);
178 assert_int_equal(clone->hash, original->hash);
179 assert_true(clone->enabled);
180
181 // ... while the snapshot still names the original, which it alone now keeps alive.
182 assert_ptr_equal(g_list_nth_data(snap.items, 0), original);
183 assert_int_equal(_refs(original), 1);
184
185 // A mutation of the clone is invisible through the snapshot -- the whole point.
186 clone->enabled = FALSE;
187 clone->hash = 0x2;
188 assert_true(((dt_dev_history_item_t *)g_list_nth_data(snap.items, 0))->enabled);
189 assert_int_equal(((dt_dev_history_item_t *)g_list_nth_data(snap.items, 0))->hash, 0x1);
190
191 dt_dev_history_snapshot_release(&snap); // drops the original's last reference
192}
193
195{
196 _fixture_t *f = *state;
197 dt_dev_history_item_t *original = _append(&f->dev, 0x1);
198 f->dev.history_end = 1;
199
200 // The pipe recorded this item as its last-synced marker, holding a reference on it, the
201 // way _pipe_set_last_history_item() does at the end of a resync.
202 dt_dev_history_item_ref(original);
203 dt_atomic_set_ptr(&f->pipe.last_history_item, original);
204 assert_int_equal(_refs(original), 2); // history + pipe
205
207 dt_pthread_rwlock_rdlock(&f->dev.history_mutex);
208 dt_dev_history_snapshot_take(&f->dev, &snap);
209 dt_pthread_rwlock_unlock(&f->dev.history_mutex);
210 assert_int_equal(_refs(original), 3); // history + pipe + snapshot
211
212 dt_dev_history_item_t *clone = dt_dev_history_cow_touch(&f->dev, original);
213 assert_ptr_not_equal(clone, original);
214
215 // The marker followed the clone, so an in-place top-entry rewrite keeps its bounded resync;
216 // the reference the marker held on the original moved with it.
217 assert_ptr_equal(dt_atomic_get_ptr(&f->pipe.last_history_item), clone);
218 assert_int_equal(_refs(clone), 2); // history + pipe
219 assert_int_equal(_refs(original), 1); // snapshot only
220
222
223 // Dropping the pipe's marker the way dt_dev_pixelpipe_cleanup() does leaves the clone
224 // owned by history alone: nothing leaked, nothing double-released.
225 dt_dev_free_history_item(dt_atomic_exch_ptr(&f->pipe.last_history_item, NULL));
226 assert_int_equal(_refs(clone), 1);
227}
228
230{
231 _fixture_t *f = *state;
232 dt_dev_history_item_t *first = _append(&f->dev, 0x1);
233 dt_dev_history_item_t *second = _append(&f->dev, 0x2);
234 f->dev.history_end = 2;
235
237 dt_atomic_set_ptr(&f->pipe.last_history_item, first);
238
240 dt_pthread_rwlock_rdlock(&f->dev.history_mutex);
241 dt_dev_history_snapshot_take(&f->dev, &snap);
242 dt_pthread_rwlock_unlock(&f->dev.history_mutex);
243
244 dt_dev_history_item_t *clone = dt_dev_history_cow_touch(&f->dev, second);
245 assert_ptr_not_equal(clone, second);
246
247 // The marker named `first', not the item being cloned: it must not move.
248 assert_ptr_equal(dt_atomic_get_ptr(&f->pipe.last_history_item), first);
249 assert_int_equal(_refs(first), 3); // history + pipe + snapshot
250 assert_int_equal(_refs(clone), 1); // history
251 assert_int_equal(_refs(second), 1); // snapshot
252
254}
255
256int main(void)
257{
258 const struct CMUnitTest tests[] = {
261 cmocka_unit_test_setup_teardown(_cow_touch_leaves_an_exclusively_owned_item_alone, _setup, _teardown),
262 cmocka_unit_test_setup_teardown(_cow_touch_clones_an_item_a_snapshot_is_holding, _setup, _teardown),
265 };
266
267 return cmocka_run_group_tests(tests, NULL, NULL);
268}
269
270// clang-format off
271// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
272// vim: shiftwidth=2 expandtab tabstop=2 cindent
273// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
274// clang-format on
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
int dt_atomic_get_int(dt_atomic_int *var)
static void * dt_atomic_exch_ptr(dt_atomic_ptr *var, void *value)
Definition atomic.h:80
static void * dt_atomic_get_ptr(const dt_atomic_ptr *var)
Definition atomic.h:76
static void dt_atomic_set_ptr(dt_atomic_ptr *var, void *value)
Definition atomic.h:75
const float f
void dt_dev_history_snapshot_release(dt_dev_history_snapshot_t *snapshot)
Release every reference a snapshot holds and reset it. Needs no lock.
dt_dev_history_item_t * dt_dev_history_item_create(void)
Allocate a fresh, blank history item with refcount 1.
void dt_dev_free_history_item(gpointer data)
Release a reference to a history item (used as GList free callback).
dt_dev_history_item_t * dt_dev_history_item_ref(dt_dev_history_item_t *item)
Take a reference on a history item. Pair with dt_dev_free_history_item().
void dt_dev_history_snapshot_take(dt_develop_t *dev, dt_dev_history_snapshot_t *snapshot) REQUIRES_SHARED(dev -> history_mutex)
Take a snapshot of dev->history. Caller holds history_mutex (reader suffices).
dt_dev_history_item_t * dt_dev_history_cow_touch(dt_develop_t *dev, dt_dev_history_item_t *hist)
Copy-on-write gate for mutating a history item in place.
static void dt_dev_set_history_hash(dt_develop_t *dev, const uint64_t history_hash)
Definition develop.h:499
static int dt_pthread_rwlock_unlock(dt_pthread_rwlock_t *rwlock) RELEASE_GENERIC(rwlock) NO_THREAD_SAFETY_ANALYSIS
Definition dtpthread.h:217
static int dt_pthread_rwlock_rdlock(dt_pthread_rwlock_t *rwlock) ACQUIRE_SHARED(rwlock) NO_THREAD_SAFETY_ANALYSIS
Definition dtpthread.h:267
static int dt_pthread_rwlock_destroy(dt_pthread_rwlock_t *lock)
Definition dtpthread.h:212
static int dt_pthread_rwlock_init(dt_pthread_rwlock_t *lock, const pthread_rwlockattr_t *attr)
Definition dtpthread.h:192
const float uint32_t state[4]
unsigned __int64 uint64_t
Definition strptime.c:75
dt_dev_pixelpipe_t pipe
struct dt_iop_module_t *gboolean enabled
Definition dev_history.h:52
A reference-sharing snapshot of dev->history, for readers that outlive a lock.
int32_t history_end
Definition develop.h:256
GList * history
Definition develop.h:259
struct dt_develop_t * dev
Definition imageop.h:311
static void _history_end_is_clamped_to_the_list_at_snapshot_time(void **state)
static void _cow_touch_leaves_an_exclusively_owned_item_alone(void **state)
static int _setup(void **state)
static int _teardown(void **state)
static void _a_snapshot_references_every_item_and_releases_them(void **state)
static void _cow_touch_ignores_a_marker_naming_a_different_item(void **state)
int main(void)
static void _cow_touch_repoints_the_pipe_marker_and_balances_every_reference(void **state)
static void _cow_touch_clones_an_item_a_snapshot_is_holding(void **state)
static dt_dev_history_item_t * _append(dt_develop_t *dev, const uint64_t hash)
static int _refs(const dt_dev_history_item_t *item)