tests: add unit tests for RamDiscardManager multi-source aggregation
Add various unit tests for the RamDiscardManager multi-source aggregation functionality. The test uses a TestRamDiscardSource QOM object that tracks populated state via a bitmap, similar to RamBlockAttributes implementation. Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com> Link: https://lore.kernel.org/r/20260604-rdm5-v5-10-5768e6a0943d@redhat.com Signed-off-by: Peter Xu <peterx@redhat.com>
Marc-André Lureau committed
Jun 4, 2026 at 17:43 UTC
c2237aaf6dcdef9a9886baee512ca2c43f3fed08
4 files changed
+1292
-1
MAINTAINERS
+2
@@ -3363,6 +3363,8 @@ F: system/memory-internal.h
3363
F: system/ram-block-attributes.c
3364
F: system/ram-discard-manager.c
3365
F: scripts/coccinelle/memory-region-housekeeping.cocci
3366
+F: tests/unit/test-ram-discard-manager.c
3367
+F: tests/unit/test-ram-discard-manager-stubs.c
3368
3369
Memory devices
3370
M: David Hildenbrand <david@kernel.org>
tests/unit/meson.build
+7
-1
@@ -138,7 +138,13 @@ if have_system
138
'test-bufferiszero': [],
139
'test-smp-parse': [qom, meson.project_source_root() / 'hw/core/machine-smp.c'],
140
'test-vmstate': [migration, io],
141
- 'test-yank': ['socket-helpers.c', qom, io, chardev]
141
+ 'test-yank': ['socket-helpers.c', qom, io, chardev],
142
+ 'test-ram-discard-manager': [
143
+ 'test-ram-discard-manager.c',
144
+ 'test-ram-discard-manager-stubs.c',
145
+ meson.project_source_root() / 'system/ram-discard-manager.c',
146
+ genh, qemuutil, qom
147
+ ],
148
}
149
if config_host_data.get('CONFIG_INOTIFY1')
150
tests += {'test-util-filemonitor': []}
tests/unit/test-ram-discard-manager-stubs.c
new
+48
@@ -0,0 +1,48 @@
1
+/* SPDX-License-Identifier: GPL-2.0-or-later */
2
+#include "qemu/osdep.h"
3
+#include "qom/object.h"
4
+#include "glib.h"
5
+#include "system/memory.h"
6
+
7
+RamDiscardManager *memory_region_get_ram_discard_manager(MemoryRegion *mr)
8
+{
9
+ return mr->rdm;
10
+}
11
+
12
+int memory_region_add_ram_discard_source(MemoryRegion *mr,
13
+ RamDiscardSource *source)
14
+{
15
+ if (!mr->rdm) {
16
+ mr->rdm = ram_discard_manager_new(mr);
17
+ }
18
+ return ram_discard_manager_add_source(mr->rdm, source);
19
+}
20
+
21
+int memory_region_del_ram_discard_source(MemoryRegion *mr,
22
+ RamDiscardSource *source)
23
+{
24
+ RamDiscardManager *rdm = mr->rdm;
25
+
26
+ if (!rdm) {
27
+ return 0;
28
+ }
29
+
30
+ return ram_discard_manager_del_source(rdm, source);
31
+}
32
+
33
+uint64_t memory_region_size(const MemoryRegion *mr)
34
+{
35
+ return int128_get64(mr->size);
36
+}
37
+
38
+MemoryRegionSection *memory_region_section_new_copy(MemoryRegionSection *s)
39
+{
40
+ MemoryRegionSection *copy = g_new(MemoryRegionSection, 1);
41
+ *copy = *s;
42
+ return copy;
43
+}
44
+
45
+void memory_region_section_free_copy(MemoryRegionSection *s)
46
+{
47
+ g_free(s);
48
+}
tests/unit/test-ram-discard-manager.c
new
+1235
@@ -0,0 +1,1235 @@
1
+/* SPDX-License-Identifier: GPL-2.0-or-later */
2
+#include "qemu/osdep.h"
3
+#include "qemu/bitmap.h"
4
+#include "qemu/module.h"
5
+#include "qemu/main-loop.h"
6
+#include "qapi/error.h"
7
+#include "qom/object.h"
8
+#include "qom/qom-qobject.h"
9
+#include "glib.h"
10
+#include "system/memory.h"
11
+
12
+#define TYPE_TEST_RAM_DISCARD_SOURCE "test-ram-discard-source"
13
+
14
+OBJECT_DECLARE_SIMPLE_TYPE(TestRamDiscardSource, TEST_RAM_DISCARD_SOURCE)
15
+
16
+struct TestRamDiscardSource {
17
+ Object parent;
18
+
19
+ MemoryRegion *mr;
20
+ uint64_t granularity;
21
+ unsigned long *bitmap;
22
+ uint64_t bitmap_size;
23
+};
24
+
25
+static uint64_t test_rds_get_min_granularity(const RamDiscardSource *rds,
26
+ const MemoryRegion *mr)
27
+{
28
+ TestRamDiscardSource *src = TEST_RAM_DISCARD_SOURCE(rds);
29
+
30
+ g_assert(mr == src->mr);
31
+ return src->granularity;
32
+}
33
+
34
+static bool test_rds_is_populated(const RamDiscardSource *rds,
35
+ const MemoryRegionSection *section)
36
+{
37
+ TestRamDiscardSource *src = TEST_RAM_DISCARD_SOURCE(rds);
38
+ uint64_t offset = section->offset_within_region;
39
+ uint64_t size = int128_get64(section->size);
40
+ uint64_t first_bit = offset / src->granularity;
41
+ uint64_t last_bit = (offset + size - 1) / src->granularity;
42
+ unsigned long found;
43
+
44
+ g_assert(section->mr == src->mr);
45
+
46
+ /* Check if any bit in range is zero (discarded) */
47
+ found = find_next_zero_bit(src->bitmap, last_bit + 1, first_bit);
48
+ return found > last_bit;
49
+}
50
+
51
+static void test_rds_class_init(ObjectClass *klass, const void *data)
52
+{
53
+ RamDiscardSourceClass *rdsc = RAM_DISCARD_SOURCE_CLASS(klass);
54
+
55
+ rdsc->get_min_granularity = test_rds_get_min_granularity;
56
+ rdsc->is_populated = test_rds_is_populated;
57
+}
58
+
59
+static const TypeInfo test_rds_info = {
60
+ .name = TYPE_TEST_RAM_DISCARD_SOURCE,
61
+ .parent = TYPE_OBJECT,
62
+ .instance_size = sizeof(TestRamDiscardSource),
63
+ .class_init = test_rds_class_init,
64
+ .interfaces = (const InterfaceInfo[]) {
65
+ { TYPE_RAM_DISCARD_SOURCE },
66
+ { }
67
+ },
68
+};
69
+
70
+static TestRamDiscardSource *test_source_new(MemoryRegion *mr,
71
+ uint64_t granularity)
72
+{
73
+ TestRamDiscardSource *src;
74
+ uint64_t region_size = memory_region_size(mr);
75
+
76
+ src = TEST_RAM_DISCARD_SOURCE(object_new(TYPE_TEST_RAM_DISCARD_SOURCE));
77
+ src->mr = mr;
78
+ src->granularity = granularity;
79
+ src->bitmap_size = DIV_ROUND_UP(region_size, granularity);
80
+ src->bitmap = bitmap_new(src->bitmap_size);
81
+
82
+ return src;
83
+}
84
+
85
+static void test_source_free(TestRamDiscardSource *src)
86
+{
87
+ g_free(src->bitmap);
88
+ object_unref(OBJECT(src));
89
+}
90
+
91
+static void test_source_populate(TestRamDiscardSource *src,
92
+ uint64_t offset, uint64_t size)
93
+{
94
+ uint64_t first_bit = offset / src->granularity;
95
+ uint64_t nbits = size / src->granularity;
96
+
97
+ bitmap_set(src->bitmap, first_bit, nbits);
98
+}
99
+
100
+static void test_source_discard(TestRamDiscardSource *src,
101
+ uint64_t offset, uint64_t size)
102
+{
103
+ uint64_t first_bit = offset / src->granularity;
104
+ uint64_t nbits = size / src->granularity;
105
+
106
+ bitmap_clear(src->bitmap, first_bit, nbits);
107
+}
108
+
109
+typedef struct TestListener {
110
+ RamDiscardListener rdl;
111
+ int populate_count;
112
+ int discard_count;
113
+ uint64_t last_populate_offset;
114
+ uint64_t last_populate_size;
115
+ uint64_t last_discard_offset;
116
+ uint64_t last_discard_size;
117
+ int fail_on_populate; /* Return error on Nth populate */
118
+ int populate_call_num;
119
+} TestListener;
120
+
121
+static int test_listener_populate(RamDiscardListener *rdl,
122
+ const MemoryRegionSection *section)
123
+{
124
+ TestListener *tl = container_of(rdl, TestListener, rdl);
125
+
126
+ tl->populate_call_num++;
127
+ if (tl->fail_on_populate > 0 &&
128
+ tl->populate_call_num >= tl->fail_on_populate) {
129
+ return -ENOMEM;
130
+ }
131
+
132
+ tl->populate_count++;
133
+ tl->last_populate_offset = section->offset_within_region;
134
+ tl->last_populate_size = int128_get64(section->size);
135
+ return 0;
136
+}
137
+
138
+static void test_listener_discard(RamDiscardListener *rdl,
139
+ const MemoryRegionSection *section)
140
+{
141
+ TestListener *tl = container_of(rdl, TestListener, rdl);
142
+
143
+ tl->discard_count++;
144
+ tl->last_discard_offset = section->offset_within_region;
145
+ tl->last_discard_size = int128_get64(section->size);
146
+}
147
+
148
+static void test_listener_init(TestListener *tl)
149
+{
150
+ ram_discard_listener_init(&tl->rdl,
151
+ test_listener_populate,
152
+ test_listener_discard);
153
+}
154
+
155
+#define TEST_REGION_SIZE (16 * 1024 * 1024) /* 16 MB */
156
+#define GRANULARITY_4K (4 * 1024)
157
+#define GRANULARITY_2M (2 * 1024 * 1024)
158
+
159
+static MemoryRegion *test_mr;
160
+
161
+static void test_setup(void)
162
+{
163
+ test_mr = g_new0(MemoryRegion, 1);
164
+ test_mr->size = int128_make64(TEST_REGION_SIZE);
165
+ test_mr->ram = true;
166
+}
167
+
168
+static void test_teardown(void)
169
+{
170
+ g_clear_pointer(&test_mr->rdm, object_unref);
171
+ object_unparent(OBJECT(test_mr));
172
+ g_free(test_mr);
173
+ test_mr = NULL;
174
+}
175
+
176
+static void test_single_source_basic(void)
177
+{
178
+ TestRamDiscardSource *src;
179
+ RamDiscardManager *rdm;
180
+ MemoryRegionSection section;
181
+ int ret;
182
+
183
+ test_setup();
184
+
185
+ src = test_source_new(test_mr, GRANULARITY_4K);
186
+ rdm = memory_region_get_ram_discard_manager(test_mr);
187
+ g_assert_null(rdm);
188
+
189
+ /* Add source */
190
+ ret = memory_region_add_ram_discard_source(test_mr,
191
+ RAM_DISCARD_SOURCE(src));
192
+ g_assert_cmpint(ret, ==, 0);
193
+
194
+ rdm = memory_region_get_ram_discard_manager(test_mr);
195
+ g_assert_nonnull(rdm);
196
+
197
+ g_assert_cmpuint(ram_discard_manager_get_min_granularity(rdm, test_mr),
198
+ ==, GRANULARITY_4K);
199
+
200
+ /* Initially all discarded */
201
+ section.mr = test_mr;
202
+ section.offset_within_region = 0;
203
+ section.size = int128_make64(GRANULARITY_4K);
204
+ g_assert_false(ram_discard_manager_is_populated(rdm, §ion));
205
+
206
+ /* Populate a range in source */
207
+ test_source_populate(src, 0, GRANULARITY_4K * 4);
208
+
209
+ /* Now should be populated */
210
+ g_assert_true(ram_discard_manager_is_populated(rdm, §ion));
211
+
212
+ /* Check larger section */
213
+ section.size = int128_make64(GRANULARITY_4K * 4);
214
+ g_assert_true(ram_discard_manager_is_populated(rdm, §ion));
215
+
216
+ /* Check section that spans populated and discarded */
217
+ section.size = int128_make64(GRANULARITY_4K * 8);
218
+ g_assert_false(ram_discard_manager_is_populated(rdm, §ion));
219
+
220
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src));
221
+
222
+ g_assert_true(ram_discard_manager_is_populated(rdm, §ion));
223
+
224
+ test_source_free(src);
225
+ test_teardown();
226
+}
227
+
228
+static void test_single_source_listener(void)
229
+{
230
+ TestRamDiscardSource *src;
231
+ RamDiscardManager *rdm;
232
+ MemoryRegionSection section;
233
+ TestListener tl = { 0, };
234
+ int ret;
235
+
236
+ test_setup();
237
+
238
+ src = test_source_new(test_mr, GRANULARITY_4K);
239
+
240
+ /* Populate some ranges before adding listener */
241
+ test_source_populate(src, 0, GRANULARITY_4K * 4);
242
+ test_source_populate(src, GRANULARITY_4K * 8, GRANULARITY_4K * 4);
243
+
244
+ ret = memory_region_add_ram_discard_source(test_mr,
245
+ RAM_DISCARD_SOURCE(src));
246
+ g_assert_cmpint(ret, ==, 0);
247
+ rdm = memory_region_get_ram_discard_manager(test_mr);
248
+ g_assert_nonnull(rdm);
249
+
250
+ /* Register listener */
251
+ test_listener_init(&tl);
252
+ section.mr = test_mr;
253
+ section.offset_within_region = 0;
254
+ section.size = int128_make64(TEST_REGION_SIZE);
255
+
256
+ ram_discard_manager_register_listener(rdm, &tl.rdl, §ion);
257
+
258
+ /* Should have been notified about populated regions */
259
+ g_assert_cmpint(tl.populate_count, ==, 2);
260
+
261
+ /* Notify populate for new range */
262
+ tl.populate_count = 0;
263
+ test_source_populate(src, GRANULARITY_4K * 16, GRANULARITY_4K * 2);
264
+ ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src),
265
+ GRANULARITY_4K * 16,
266
+ GRANULARITY_4K * 2);
267
+ g_assert_cmpint(ret, ==, 0);
268
+ g_assert_cmpint(tl.populate_count, ==, 1);
269
+ g_assert_cmpuint(tl.last_populate_offset, ==, GRANULARITY_4K * 16);
270
+ g_assert_cmpuint(tl.last_populate_size, ==, GRANULARITY_4K * 2);
271
+
272
+ /* Notify discard */
273
+ tl.discard_count = 0;
274
+ test_source_discard(src, 0, GRANULARITY_4K * 4);
275
+ ram_discard_manager_notify_discard(rdm, RAM_DISCARD_SOURCE(src),
276
+ 0, GRANULARITY_4K * 4);
277
+ g_assert_cmpint(tl.discard_count, ==, 1);
278
+ g_assert_cmpuint(tl.last_discard_offset, ==, 0);
279
+ g_assert_cmpuint(tl.last_discard_size, ==, GRANULARITY_4K * 4);
280
+
281
+ /* Unregister listener */
282
+ ram_discard_manager_unregister_listener(rdm, &tl.rdl);
283
+
284
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src));
285
+ test_source_free(src);
286
+ test_teardown();
287
+}
288
+
289
+static void test_two_sources_same_granularity(void)
290
+{
291
+ TestRamDiscardSource *src1, *src2;
292
+ RamDiscardManager *rdm;
293
+ MemoryRegionSection section;
294
+ int ret;
295
+
296
+ test_setup();
297
+
298
+ src1 = test_source_new(test_mr, GRANULARITY_4K);
299
+ src2 = test_source_new(test_mr, GRANULARITY_4K);
300
+
301
+ /* Add first source */
302
+ ret = memory_region_add_ram_discard_source(test_mr,
303
+ RAM_DISCARD_SOURCE(src1));
304
+ g_assert_cmpint(ret, ==, 0);
305
+
306
+ /* Add second source */
307
+ ret = memory_region_add_ram_discard_source(test_mr,
308
+ RAM_DISCARD_SOURCE(src2));
309
+ g_assert_cmpint(ret, ==, 0);
310
+
311
+ rdm = memory_region_get_ram_discard_manager(test_mr);
312
+ g_assert_nonnull(rdm);
313
+
314
+ /* Check granularity */
315
+ g_assert_cmpuint(ram_discard_manager_get_min_granularity(rdm, test_mr),
316
+ ==, GRANULARITY_4K);
317
+
318
+ section.mr = test_mr;
319
+ section.offset_within_region = 0;
320
+ section.size = int128_make64(GRANULARITY_4K);
321
+
322
+ /* Both discarded -> aggregated discarded */
323
+ g_assert_false(ram_discard_manager_is_populated(rdm, §ion));
324
+
325
+ /* Populate in src1 only */
326
+ test_source_populate(src1, 0, GRANULARITY_4K);
327
+ g_assert_false(ram_discard_manager_is_populated(rdm, §ion));
328
+
329
+ /* Populate in src2 only */
330
+ test_source_discard(src1, 0, GRANULARITY_4K);
331
+ test_source_populate(src2, 0, GRANULARITY_4K);
332
+ g_assert_false(ram_discard_manager_is_populated(rdm, §ion));
333
+
334
+ /* Populate in both -> aggregated populated */
335
+ test_source_populate(src1, 0, GRANULARITY_4K);
336
+ g_assert_true(ram_discard_manager_is_populated(rdm, §ion));
337
+
338
+ /* Remove sources */
339
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2));
340
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1));
341
+
342
+ test_source_free(src2);
343
+ test_source_free(src1);
344
+ test_teardown();
345
+}
346
+
347
+/*
348
+ * Test: Two sources with different granularities (4K and 2M).
349
+ * The aggregated granularity should be GCD(4K, 2M) = 4K.
350
+ */
351
+static void test_two_sources_different_granularity(void)
352
+{
353
+ TestRamDiscardSource *src_4k, *src_2m;
354
+ RamDiscardManager *rdm;
355
+ MemoryRegionSection section;
356
+ int ret;
357
+
358
+ test_setup();
359
+
360
+ src_4k = test_source_new(test_mr, GRANULARITY_4K);
361
+ src_2m = test_source_new(test_mr, GRANULARITY_2M);
362
+
363
+ ret = memory_region_add_ram_discard_source(test_mr,
364
+ RAM_DISCARD_SOURCE(src_4k));
365
+ g_assert_cmpint(ret, ==, 0);
366
+
367
+ ret = memory_region_add_ram_discard_source(test_mr,
368
+ RAM_DISCARD_SOURCE(src_2m));
369
+ g_assert_cmpint(ret, ==, 0);
370
+
371
+ rdm = memory_region_get_ram_discard_manager(test_mr);
372
+
373
+ g_assert_cmpuint(ram_discard_manager_get_min_granularity(rdm, test_mr),
374
+ ==, GRANULARITY_4K);
375
+
376
+ section.mr = test_mr;
377
+ section.offset_within_region = 0;
378
+ section.size = int128_make64(GRANULARITY_4K);
379
+
380
+ /* Both discarded */
381
+ g_assert_false(ram_discard_manager_is_populated(rdm, §ion));
382
+
383
+ /* Populate 4K in src_4k, but src_2m still discarded the whole 2M block */
384
+ test_source_populate(src_4k, 0, GRANULARITY_4K);
385
+ g_assert_false(ram_discard_manager_is_populated(rdm, §ion));
386
+
387
+ /* Populate 2M in src_2m (which includes the 4K block) */
388
+ test_source_populate(src_2m, 0, GRANULARITY_2M);
389
+ g_assert_true(ram_discard_manager_is_populated(rdm, §ion));
390
+
391
+ /* Check a 4K block at offset 4K (populated in src_2m but not in src_4k) */
392
+ section.offset_within_region = GRANULARITY_4K;
393
+ g_assert_false(ram_discard_manager_is_populated(rdm, §ion));
394
+
395
+ /* Populate it in src_4k */
396
+ test_source_populate(src_4k, GRANULARITY_4K, GRANULARITY_4K);
397
+ g_assert_true(ram_discard_manager_is_populated(rdm, §ion));
398
+
399
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src_2m));
400
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src_4k));
401
+
402
+ test_source_free(src_2m);
403
+ test_source_free(src_4k);
404
+ test_teardown();
405
+}
406
+
407
+/*
408
+ * Test: Notification with two sources.
409
+ * Populate notification should only fire when all sources are populated.
410
+ */
411
+static void test_two_sources_notification(void)
412
+{
413
+ TestRamDiscardSource *src1, *src2;
414
+ RamDiscardManager *rdm;
415
+ MemoryRegionSection section;
416
+ TestListener tl = { 0, };
417
+ int ret;
418
+
419
+ test_setup();
420
+
421
+ src1 = test_source_new(test_mr, GRANULARITY_4K);
422
+ src2 = test_source_new(test_mr, GRANULARITY_4K);
423
+
424
+ ret = memory_region_add_ram_discard_source(test_mr,
425
+ RAM_DISCARD_SOURCE(src1));
426
+ g_assert_cmpint(ret, ==, 0);
427
+ ret = memory_region_add_ram_discard_source(test_mr,
428
+ RAM_DISCARD_SOURCE(src2));
429
+ g_assert_cmpint(ret, ==, 0);
430
+
431
+ rdm = memory_region_get_ram_discard_manager(test_mr);
432
+
433
+ /* Register listener */
434
+ test_listener_init(&tl);
435
+ section.mr = test_mr;
436
+ section.offset_within_region = 0;
437
+ section.size = int128_make64(TEST_REGION_SIZE);
438
+ ram_discard_manager_register_listener(rdm, &tl.rdl, §ion);
439
+
440
+ /* No populate notifications yet (all discarded) */
441
+ g_assert_cmpint(tl.populate_count, ==, 0);
442
+
443
+ /* Populate in src1 only - no notification (src2 still discarded) */
444
+ test_source_populate(src1, 0, GRANULARITY_4K * 4);
445
+ ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src1),
446
+ 0, GRANULARITY_4K * 4);
447
+ g_assert_cmpint(ret, ==, 0);
448
+ g_assert_cmpint(tl.populate_count, ==, 0);
449
+
450
+ /* Populate same range in src2 - now should notify */
451
+ test_source_populate(src2, 0, GRANULARITY_4K * 4);
452
+ ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src2),
453
+ 0, GRANULARITY_4K * 4);
454
+ g_assert_cmpint(ret, ==, 0);
455
+ g_assert_cmpint(tl.populate_count, ==, 1);
456
+
457
+ /* Discard from src1 - should notify discard immediately */
458
+ tl.discard_count = 0;
459
+ test_source_discard(src1, 0, GRANULARITY_4K * 2);
460
+ ram_discard_manager_notify_discard(rdm, RAM_DISCARD_SOURCE(src1),
461
+ 0, GRANULARITY_4K * 2);
462
+ g_assert_cmpint(tl.discard_count, ==, 1);
463
+
464
+ ram_discard_manager_unregister_listener(rdm, &tl.rdl);
465
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2));
466
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1));
467
+
468
+ test_source_free(src2);
469
+ test_source_free(src1);
470
+ test_teardown();
471
+}
472
+
473
+/*
474
+ * Test: Adding source with existing listener.
475
+ * When a new source is added, listeners should be notified about
476
+ * regions that become discarded.
477
+ */
478
+static void test_add_source_with_listener(void)
479
+{
480
+ TestRamDiscardSource *src1, *src2;
481
+ RamDiscardManager *rdm;
482
+ MemoryRegionSection section;
483
+ TestListener tl = { 0, };
484
+ int ret;
485
+
486
+ test_setup();
487
+
488
+ src1 = test_source_new(test_mr, GRANULARITY_4K);
489
+ src2 = test_source_new(test_mr, GRANULARITY_4K);
490
+
491
+ /* Populate some range in src1 */
492
+ test_source_populate(src1, 0, GRANULARITY_4K * 8);
493
+
494
+ ret = memory_region_add_ram_discard_source(test_mr,
495
+ RAM_DISCARD_SOURCE(src1));
496
+ g_assert_cmpint(ret, ==, 0);
497
+ rdm = memory_region_get_ram_discard_manager(test_mr);
498
+
499
+ /* Register listener */
500
+ test_listener_init(&tl);
501
+ section.mr = test_mr;
502
+ section.offset_within_region = 0;
503
+ section.size = int128_make64(TEST_REGION_SIZE);
504
+ ram_discard_manager_register_listener(rdm, &tl.rdl, §ion);
505
+
506
+ /* Should have been notified about populated region */
507
+ g_assert_cmpint(tl.populate_count, ==, 1);
508
+ g_assert_cmpint(tl.last_populate_offset, ==, 0);
509
+ g_assert_cmpint(tl.last_populate_size, ==, GRANULARITY_4K * 8);
510
+
511
+ /* src2 has part of the region populated, part discarded */
512
+ /* src2 has 0-4 populated, 4-8 discarded */
513
+ test_source_populate(src2, 0, GRANULARITY_4K * 4);
514
+
515
+ /* Add src2 - listener should be notified about newly discarded regions */
516
+ tl.discard_count = 0;
517
+ ret = memory_region_add_ram_discard_source(test_mr,
518
+ RAM_DISCARD_SOURCE(src2));
519
+ g_assert_cmpint(ret, ==, 0);
520
+
521
+ /*
522
+ * The range 4K*4 to 4K*8 was populated in src1 but discarded in src2,
523
+ * so it becomes aggregated-discarded. Listener should be notified.
524
+ * Only this range should trigger a discard notification - regions beyond
525
+ * 4K*8 were already discarded in src1, so adding src2 doesn't change them.
526
+ */
527
+ g_assert_cmpint(tl.discard_count, ==, 1);
528
+ g_assert_cmpint(tl.last_discard_offset, ==, GRANULARITY_4K * 4);
529
+ g_assert_cmpint(tl.last_discard_size, ==, GRANULARITY_4K * 4);
530
+
531
+ ram_discard_manager_unregister_listener(rdm, &tl.rdl);
532
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2));
533
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1));
534
+
535
+ test_source_free(src2);
536
+ test_source_free(src1);
537
+ test_teardown();
538
+}
539
+
540
+/*
541
+ * Test: Removing source with existing listener.
542
+ * When a source is removed, listeners should be notified about
543
+ * regions that become populated.
544
+ */
545
+static void test_remove_source_with_listener(void)
546
+{
547
+ TestRamDiscardSource *src1, *src2;
548
+ RamDiscardManager *rdm;
549
+ MemoryRegionSection section;
550
+ TestListener tl = { 0, };
551
+ int ret;
552
+
553
+ test_setup();
554
+
555
+ src1 = test_source_new(test_mr, GRANULARITY_4K);
556
+ src2 = test_source_new(test_mr, GRANULARITY_4K);
557
+
558
+ /* src1: all of first 8 blocks populated */
559
+ test_source_populate(src1, 0, GRANULARITY_4K * 8);
560
+ /* src2: only first 4 blocks populated */
561
+ test_source_populate(src2, 0, GRANULARITY_4K * 4);
562
+
563
+ ret = memory_region_add_ram_discard_source(test_mr,
564
+ RAM_DISCARD_SOURCE(src1));
565
+ g_assert_cmpint(ret, ==, 0);
566
+ ret = memory_region_add_ram_discard_source(test_mr,
567
+ RAM_DISCARD_SOURCE(src2));
568
+ g_assert_cmpint(ret, ==, 0);
569
+
570
+ rdm = memory_region_get_ram_discard_manager(test_mr);
571
+
572
+ /* Register listener */
573
+ test_listener_init(&tl);
574
+ section.mr = test_mr;
575
+ section.offset_within_region = 0;
576
+ section.size = int128_make64(TEST_REGION_SIZE);
577
+ ram_discard_manager_register_listener(rdm, &tl.rdl, §ion);
578
+
579
+ /* Only first 4 blocks are aggregated-populated */
580
+ g_assert_cmpint(tl.populate_count, ==, 1);
581
+ g_assert_cmpuint(tl.last_populate_size, ==, GRANULARITY_4K * 4);
582
+
583
+ /* Remove src2 - blocks 4-8 should become populated */
584
+ tl.populate_count = 0;
585
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2));
586
+
587
+ /* Listener should be notified about newly populated region (4K*4 to 4K*8) */
588
+ g_assert_cmpint(tl.populate_count, >=, 1);
589
+
590
+ ram_discard_manager_unregister_listener(rdm, &tl.rdl);
591
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1));
592
+
593
+ test_source_free(src2);
594
+ test_source_free(src1);
595
+ test_teardown();
596
+}
597
+
598
+/*
599
+ * Test: Add a source, register a listener, remove the source, then add it back.
600
+ * This checks the transition from 0 sources (all populated) to 1 source
601
+ * (partially discarded) with an active listener.
602
+ */
603
+static void test_readd_source_with_listener(void)
604
+{
605
+ TestRamDiscardSource *src;
606
+ RamDiscardManager *rdm;
607
+ MemoryRegionSection section;
608
+ TestListener tl = { 0, };
609
+ int ret;
610
+
611
+ test_setup();
612
+
613
+ src = test_source_new(test_mr, GRANULARITY_4K);
614
+
615
+ /* Populate some range in src */
616
+ test_source_populate(src, 0, GRANULARITY_4K * 8);
617
+
618
+ /* 1. Add source */
619
+ ret = memory_region_add_ram_discard_source(test_mr,
620
+ RAM_DISCARD_SOURCE(src));
621
+ g_assert_cmpint(ret, ==, 0);
622
+ rdm = memory_region_get_ram_discard_manager(test_mr);
623
+
624
+ /* 2. Register listener */
625
+ test_listener_init(&tl);
626
+ section.mr = test_mr;
627
+ section.offset_within_region = 0;
628
+ section.size = int128_make64(TEST_REGION_SIZE);
629
+ ram_discard_manager_register_listener(rdm, &tl.rdl, §ion);
630
+
631
+ /* Listener notified about populated region (0 - 32K) */
632
+ g_assert_cmpint(tl.populate_count, ==, 1);
633
+ g_assert_cmpuint(tl.last_populate_size, ==, GRANULARITY_4K * 8);
634
+
635
+ /* 3. Remove source */
636
+ tl.populate_count = 0;
637
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src));
638
+
639
+ /*
640
+ * With 0 sources, everything is populated.
641
+ * The range that was discarded in src (from 32K to end) becomes populated.
642
+ */
643
+ g_assert_cmpint(tl.populate_count, ==, 1);
644
+ g_assert_cmpuint(tl.last_populate_offset, ==, GRANULARITY_4K * 8);
645
+ g_assert_cmpuint(tl.last_populate_size, ==, TEST_REGION_SIZE - GRANULARITY_4K * 8);
646
+
647
+ /* 4. Add source back */
648
+ tl.discard_count = 0;
649
+ ret = memory_region_add_ram_discard_source(test_mr,
650
+ RAM_DISCARD_SOURCE(src));
651
+ g_assert_cmpint(ret, ==, 0);
652
+
653
+ /*
654
+ * Now we have 1 source again. The range (32K to end) is discarded again.
655
+ * Listener should be notified about this discard.
656
+ */
657
+ g_assert_cmpint(tl.discard_count, ==, 1);
658
+ g_assert_cmpuint(tl.last_discard_offset, ==, GRANULARITY_4K * 8);
659
+ g_assert_cmpuint(tl.last_discard_size, ==, TEST_REGION_SIZE - GRANULARITY_4K * 8);
660
+
661
+ ram_discard_manager_unregister_listener(rdm, &tl.rdl);
662
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src));
663
+ test_source_free(src);
664
+ test_teardown();
665
+}
666
+
667
+/*
668
+ * Test: Duplicate source registration should fail.
669
+ */
670
+static void test_duplicate_source(void)
671
+{
672
+ TestRamDiscardSource *src;
673
+ int ret;
674
+
675
+ test_setup();
676
+
677
+ src = test_source_new(test_mr, GRANULARITY_4K);
678
+
679
+ ret = memory_region_add_ram_discard_source(test_mr,
680
+ RAM_DISCARD_SOURCE(src));
681
+ g_assert_cmpint(ret, ==, 0);
682
+
683
+ /* Adding same source again should fail */
684
+ ret = memory_region_add_ram_discard_source(test_mr,
685
+ RAM_DISCARD_SOURCE(src));
686
+ g_assert_cmpint(ret, ==, -EBUSY);
687
+
688
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src));
689
+ test_source_free(src);
690
+ test_teardown();
691
+}
692
+
693
+/*
694
+ * Test: Populate notification rollback on listener error.
695
+ */
696
+static void test_populate_rollback(void)
697
+{
698
+ TestRamDiscardSource *src;
699
+ RamDiscardManager *rdm;
700
+ MemoryRegionSection section;
701
+ TestListener tl1 = { 0, }, tl2 = { 0, };
702
+ int ret;
703
+
704
+ test_setup();
705
+
706
+ src = test_source_new(test_mr, GRANULARITY_4K);
707
+
708
+ ret = memory_region_add_ram_discard_source(test_mr,
709
+ RAM_DISCARD_SOURCE(src));
710
+ g_assert_cmpint(ret, ==, 0);
711
+ rdm = memory_region_get_ram_discard_manager(test_mr);
712
+
713
+ /* Register two listeners */
714
+ test_listener_init(&tl1);
715
+ test_listener_init(&tl2);
716
+ tl2.fail_on_populate = 1; /* Second listener fails on first populate */
717
+
718
+ section.mr = test_mr;
719
+ section.offset_within_region = 0;
720
+ section.size = int128_make64(TEST_REGION_SIZE);
721
+
722
+ /*
723
+ * Register tl2 first so it's visited second (QLIST_INSERT_HEAD reverses
724
+ * registration order). This ensures tl1 receives populate before tl2
725
+ * fails.
726
+ */
727
+ ram_discard_manager_register_listener(rdm, &tl2.rdl, §ion);
728
+ ram_discard_manager_register_listener(rdm, &tl1.rdl, §ion);
729
+
730
+ /* Try to populate - should fail and roll back */
731
+ test_source_populate(src, 0, GRANULARITY_4K);
732
+ ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src),
733
+ 0, GRANULARITY_4K);
734
+ g_assert_cmpint(ret, ==, -ENOMEM);
735
+
736
+ /* First listener should have received populate then discard (rollback) */
737
+ g_assert_cmpint(tl1.populate_count, ==, 1);
738
+ g_assert_cmpint(tl1.discard_count, ==, 1);
739
+
740
+ ram_discard_manager_unregister_listener(rdm, &tl1.rdl);
741
+ ram_discard_manager_unregister_listener(rdm, &tl2.rdl);
742
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src));
743
+ test_source_free(src);
744
+ test_teardown();
745
+}
746
+
747
+/*
748
+ * Test: Replay populated with two sources (intersection).
749
+ */
750
+static void test_replay_populated_intersection(void)
751
+{
752
+ TestRamDiscardSource *src1, *src2;
753
+ RamDiscardManager *rdm;
754
+ MemoryRegionSection section;
755
+ TestListener tl = { 0, };
756
+ int ret;
757
+
758
+ test_setup();
759
+
760
+ src1 = test_source_new(test_mr, GRANULARITY_4K);
761
+ src2 = test_source_new(test_mr, GRANULARITY_4K);
762
+
763
+ /*
764
+ * src1: blocks 0-7 populated
765
+ * src2: blocks 4-11 populated
766
+ * Intersection: blocks 4-7
767
+ */
768
+ test_source_populate(src1, 0, GRANULARITY_4K * 8);
769
+ test_source_populate(src2, GRANULARITY_4K * 4, GRANULARITY_4K * 8);
770
+
771
+ ret = memory_region_add_ram_discard_source(test_mr,
772
+ RAM_DISCARD_SOURCE(src1));
773
+ g_assert_cmpint(ret, ==, 0);
774
+ ret = memory_region_add_ram_discard_source(test_mr,
775
+ RAM_DISCARD_SOURCE(src2));
776
+ g_assert_cmpint(ret, ==, 0);
777
+
778
+ rdm = memory_region_get_ram_discard_manager(test_mr);
779
+
780
+ /* Register listener - should only get notified about intersection */
781
+ test_listener_init(&tl);
782
+ section.mr = test_mr;
783
+ section.offset_within_region = 0;
784
+ section.size = int128_make64(TEST_REGION_SIZE);
785
+ ram_discard_manager_register_listener(rdm, &tl.rdl, §ion);
786
+
787
+ /* Should have been notified about blocks 4-7 (intersection) */
788
+ g_assert_cmpint(tl.populate_count, ==, 1);
789
+ g_assert_cmpuint(tl.last_populate_offset, ==, GRANULARITY_4K * 4);
790
+ g_assert_cmpuint(tl.last_populate_size, ==, GRANULARITY_4K * 4);
791
+
792
+ ram_discard_manager_unregister_listener(rdm, &tl.rdl);
793
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2));
794
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1));
795
+
796
+ test_source_free(src2);
797
+ test_source_free(src1);
798
+ test_teardown();
799
+}
800
+
801
+/*
802
+ * Test: Empty region (no sources).
803
+ */
804
+static void test_no_sources(void)
805
+{
806
+ test_setup();
807
+
808
+ /* No sources - should have no manager */
809
+ g_assert_null(memory_region_get_ram_discard_manager(test_mr));
810
+ g_assert_false(memory_region_has_ram_discard_manager(test_mr));
811
+
812
+ test_teardown();
813
+}
814
+
815
+static void test_redundant_discard(void)
816
+{
817
+ TestRamDiscardSource *src1, *src2;
818
+ RamDiscardManager *rdm;
819
+ MemoryRegionSection section;
820
+ TestListener tl = { 0, };
821
+ int ret;
822
+
823
+ test_setup();
824
+
825
+ src1 = test_source_new(test_mr, GRANULARITY_4K);
826
+ src2 = test_source_new(test_mr, GRANULARITY_4K);
827
+
828
+ /* Add sources */
829
+ ret = memory_region_add_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1));
830
+ g_assert_cmpint(ret, ==, 0);
831
+ ret = memory_region_add_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2));
832
+ g_assert_cmpint(ret, ==, 0);
833
+
834
+ rdm = memory_region_get_ram_discard_manager(test_mr);
835
+
836
+ /* Register listener */
837
+ test_listener_init(&tl);
838
+ section.mr = test_mr;
839
+ section.offset_within_region = 0;
840
+ section.size = int128_make64(TEST_REGION_SIZE);
841
+ ram_discard_manager_register_listener(rdm, &tl.rdl, §ion);
842
+
843
+ /* Populate intersection (0-4K) in both sources */
844
+ test_source_populate(src1, 0, GRANULARITY_4K);
845
+ test_source_populate(src2, 0, GRANULARITY_4K);
846
+
847
+ /* Notify populate src1 - should trigger listener populate */
848
+ ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src1),
849
+ 0, GRANULARITY_4K);
850
+ g_assert_cmpint(ret, ==, 0);
851
+ g_assert_cmpint(tl.populate_count, ==, 1);
852
+
853
+ /* Now Discard src1 -> Aggregate Discarded */
854
+ tl.discard_count = 0;
855
+ test_source_discard(src1, 0, GRANULARITY_4K);
856
+ ram_discard_manager_notify_discard(rdm, RAM_DISCARD_SOURCE(src1), 0, GRANULARITY_4K);
857
+ g_assert_cmpint(tl.discard_count, ==, 1);
858
+
859
+ /* Now Discard src2 -> Aggregate Discarded (Already Discarded!) */
860
+ /* Listener should NOT receive another discard notification for the same range. */
861
+ test_source_discard(src2, 0, GRANULARITY_4K);
862
+ ram_discard_manager_notify_discard(rdm, RAM_DISCARD_SOURCE(src2), 0, GRANULARITY_4K);
863
+
864
+ g_assert_cmpint(tl.discard_count, ==, 1);
865
+
866
+ ram_discard_manager_unregister_listener(rdm, &tl.rdl);
867
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2));
868
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1));
869
+
870
+ test_source_free(src2);
871
+ test_source_free(src1);
872
+ test_teardown();
873
+}
874
+
875
+/*
876
+ * Test: Listener with partial section coverage.
877
+ * Listener should only receive notifications for its registered range.
878
+ */
879
+static void test_partial_listener_section(void)
880
+{
881
+ TestRamDiscardSource *src;
882
+ RamDiscardManager *rdm;
883
+ MemoryRegionSection section;
884
+ TestListener tl = { 0, };
885
+ int ret;
886
+
887
+ test_setup();
888
+
889
+ src = test_source_new(test_mr, GRANULARITY_4K);
890
+
891
+ /* Populate blocks 0-7 */
892
+ test_source_populate(src, 0, GRANULARITY_4K * 8);
893
+
894
+ ret = memory_region_add_ram_discard_source(test_mr,
895
+ RAM_DISCARD_SOURCE(src));
896
+ g_assert_cmpint(ret, ==, 0);
897
+ rdm = memory_region_get_ram_discard_manager(test_mr);
898
+
899
+ /* Register listener for only blocks 2-5 (not the full region) */
900
+ test_listener_init(&tl);
901
+ section.mr = test_mr;
902
+ section.offset_within_region = GRANULARITY_4K * 2;
903
+ section.size = int128_make64(GRANULARITY_4K * 4);
904
+ ram_discard_manager_register_listener(rdm, &tl.rdl, §ion);
905
+
906
+ /* Should be notified only about blocks 2-5 (intersection) */
907
+ g_assert_cmpint(tl.populate_count, ==, 1);
908
+ g_assert_cmpuint(tl.last_populate_offset, ==, GRANULARITY_4K * 2);
909
+ g_assert_cmpuint(tl.last_populate_size, ==, GRANULARITY_4K * 4);
910
+
911
+ /* Discard block 0 - outside listener's section, no notification */
912
+ tl.discard_count = 0;
913
+ test_source_discard(src, 0, GRANULARITY_4K);
914
+ ram_discard_manager_notify_discard(rdm, RAM_DISCARD_SOURCE(src),
915
+ 0, GRANULARITY_4K);
916
+ g_assert_cmpint(tl.discard_count, ==, 0);
917
+
918
+ /* Discard block 3 - inside listener's section */
919
+ test_source_discard(src, GRANULARITY_4K * 3, GRANULARITY_4K);
920
+ ram_discard_manager_notify_discard(rdm, RAM_DISCARD_SOURCE(src),
921
+ GRANULARITY_4K * 3, GRANULARITY_4K);
922
+ g_assert_cmpint(tl.discard_count, ==, 1);
923
+ g_assert_cmpuint(tl.last_discard_offset, ==, GRANULARITY_4K * 3);
924
+
925
+ /* Discard spanning boundary (blocks 5-6) - only block 5 in section */
926
+ tl.discard_count = 0;
927
+ test_source_discard(src, GRANULARITY_4K * 5, GRANULARITY_4K * 2);
928
+ ram_discard_manager_notify_discard(rdm, RAM_DISCARD_SOURCE(src),
929
+ GRANULARITY_4K * 5, GRANULARITY_4K * 2);
930
+ g_assert_cmpint(tl.discard_count, ==, 1);
931
+ g_assert_cmpuint(tl.last_discard_offset, ==, GRANULARITY_4K * 5);
932
+ g_assert_cmpuint(tl.last_discard_size, ==, GRANULARITY_4K);
933
+
934
+ ram_discard_manager_unregister_listener(rdm, &tl.rdl);
935
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src));
936
+ test_source_free(src);
937
+ test_teardown();
938
+}
939
+
940
+/*
941
+ * Test: Multiple listeners with different (non-overlapping) sections.
942
+ */
943
+static void test_multiple_listeners_different_sections(void)
944
+{
945
+ TestRamDiscardSource *src;
946
+ RamDiscardManager *rdm;
947
+ MemoryRegionSection section1, section2;
948
+ TestListener tl1 = { 0, }, tl2 = { 0, };
949
+ int ret;
950
+
951
+ test_setup();
952
+
953
+ src = test_source_new(test_mr, GRANULARITY_4K);
954
+
955
+ ret = memory_region_add_ram_discard_source(test_mr,
956
+ RAM_DISCARD_SOURCE(src));
957
+ g_assert_cmpint(ret, ==, 0);
958
+ rdm = memory_region_get_ram_discard_manager(test_mr);
959
+
960
+ /* Listener 1: blocks 0-3 */
961
+ test_listener_init(&tl1);
962
+ section1.mr = test_mr;
963
+ section1.offset_within_region = 0;
964
+ section1.size = int128_make64(GRANULARITY_4K * 4);
965
+ ram_discard_manager_register_listener(rdm, &tl1.rdl, §ion1);
966
+
967
+ /* Listener 2: blocks 8-11 */
968
+ test_listener_init(&tl2);
969
+ section2.mr = test_mr;
970
+ section2.offset_within_region = GRANULARITY_4K * 8;
971
+ section2.size = int128_make64(GRANULARITY_4K * 4);
972
+ ram_discard_manager_register_listener(rdm, &tl2.rdl, §ion2);
973
+
974
+ /* Initially all discarded - no populate notifications */
975
+ g_assert_cmpint(tl1.populate_count, ==, 0);
976
+ g_assert_cmpint(tl2.populate_count, ==, 0);
977
+
978
+ /* Populate blocks 0-3 - only tl1 should be notified */
979
+ test_source_populate(src, 0, GRANULARITY_4K * 4);
980
+ ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src),
981
+ 0, GRANULARITY_4K * 4);
982
+ g_assert_cmpint(ret, ==, 0);
983
+ g_assert_cmpint(tl1.populate_count, ==, 1);
984
+ g_assert_cmpint(tl2.populate_count, ==, 0);
985
+
986
+ /* Populate blocks 8-11 - only tl2 should be notified */
987
+ test_source_populate(src, GRANULARITY_4K * 8, GRANULARITY_4K * 4);
988
+ ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src),
989
+ GRANULARITY_4K * 8,
990
+ GRANULARITY_4K * 4);
991
+ g_assert_cmpint(ret, ==, 0);
992
+ g_assert_cmpint(tl1.populate_count, ==, 1);
993
+ g_assert_cmpint(tl2.populate_count, ==, 1);
994
+
995
+ /* Populate blocks 4-7 (gap) - neither listener should be notified */
996
+ test_source_populate(src, GRANULARITY_4K * 4, GRANULARITY_4K * 4);
997
+ ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src),
998
+ GRANULARITY_4K * 4,
999
+ GRANULARITY_4K * 4);
1000
+ g_assert_cmpint(ret, ==, 0);
1001
+ g_assert_cmpint(tl1.populate_count, ==, 1);
1002
+ g_assert_cmpint(tl2.populate_count, ==, 1);
1003
+
1004
+ ram_discard_manager_unregister_listener(rdm, &tl2.rdl);
1005
+ ram_discard_manager_unregister_listener(rdm, &tl1.rdl);
1006
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src));
1007
+ test_source_free(src);
1008
+ test_teardown();
1009
+}
1010
+
1011
+/*
1012
+ * Test: Multiple listeners with overlapping sections.
1013
+ */
1014
+static void test_overlapping_listener_sections(void)
1015
+{
1016
+ TestRamDiscardSource *src;
1017
+ RamDiscardManager *rdm;
1018
+ MemoryRegionSection section1, section2;
1019
+ TestListener tl1 = { 0, }, tl2 = { 0, };
1020
+ int ret;
1021
+
1022
+ test_setup();
1023
+
1024
+ src = test_source_new(test_mr, GRANULARITY_4K);
1025
+
1026
+ ret = memory_region_add_ram_discard_source(test_mr,
1027
+ RAM_DISCARD_SOURCE(src));
1028
+ g_assert_cmpint(ret, ==, 0);
1029
+ rdm = memory_region_get_ram_discard_manager(test_mr);
1030
+
1031
+ /* Listener 1: blocks 0-7 */
1032
+ test_listener_init(&tl1);
1033
+ section1.mr = test_mr;
1034
+ section1.offset_within_region = 0;
1035
+ section1.size = int128_make64(GRANULARITY_4K * 8);
1036
+ ram_discard_manager_register_listener(rdm, &tl1.rdl, §ion1);
1037
+
1038
+ /* Listener 2: blocks 4-11 (overlaps with tl1 on blocks 4-7) */
1039
+ test_listener_init(&tl2);
1040
+ section2.mr = test_mr;
1041
+ section2.offset_within_region = GRANULARITY_4K * 4;
1042
+ section2.size = int128_make64(GRANULARITY_4K * 8);
1043
+ ram_discard_manager_register_listener(rdm, &tl2.rdl, §ion2);
1044
+
1045
+ /* Populate blocks 4-7 (overlap region) - both should be notified */
1046
+ test_source_populate(src, GRANULARITY_4K * 4, GRANULARITY_4K * 4);
1047
+ ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src),
1048
+ GRANULARITY_4K * 4,
1049
+ GRANULARITY_4K * 4);
1050
+ g_assert_cmpint(ret, ==, 0);
1051
+ g_assert_cmpint(tl1.populate_count, ==, 1);
1052
+ g_assert_cmpint(tl2.populate_count, ==, 1);
1053
+
1054
+ /* Populate blocks 0-3 - only tl1 */
1055
+ test_source_populate(src, 0, GRANULARITY_4K * 4);
1056
+ ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src),
1057
+ 0, GRANULARITY_4K * 4);
1058
+ g_assert_cmpint(ret, ==, 0);
1059
+ g_assert_cmpint(tl1.populate_count, ==, 2);
1060
+ g_assert_cmpint(tl2.populate_count, ==, 1);
1061
+
1062
+ /* Populate blocks 8-11 - only tl2 */
1063
+ test_source_populate(src, GRANULARITY_4K * 8, GRANULARITY_4K * 4);
1064
+ ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src),
1065
+ GRANULARITY_4K * 8,
1066
+ GRANULARITY_4K * 4);
1067
+ g_assert_cmpint(ret, ==, 0);
1068
+ g_assert_cmpint(tl1.populate_count, ==, 2);
1069
+ g_assert_cmpint(tl2.populate_count, ==, 2);
1070
+
1071
+ ram_discard_manager_unregister_listener(rdm, &tl2.rdl);
1072
+ ram_discard_manager_unregister_listener(rdm, &tl1.rdl);
1073
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src));
1074
+ test_source_free(src);
1075
+ test_teardown();
1076
+}
1077
+
1078
+/*
1079
+ * Test: Listener at exact memory region boundaries.
1080
+ */
1081
+static void test_boundary_section(void)
1082
+{
1083
+ TestRamDiscardSource *src;
1084
+ RamDiscardManager *rdm;
1085
+ MemoryRegionSection section;
1086
+ TestListener tl = { 0, };
1087
+ uint64_t last_offset;
1088
+ int ret;
1089
+
1090
+ test_setup();
1091
+
1092
+ src = test_source_new(test_mr, GRANULARITY_4K);
1093
+
1094
+ /* Populate last 4 blocks of the region */
1095
+ last_offset = TEST_REGION_SIZE - GRANULARITY_4K * 4;
1096
+ test_source_populate(src, last_offset, GRANULARITY_4K * 4);
1097
+
1098
+ ret = memory_region_add_ram_discard_source(test_mr,
1099
+ RAM_DISCARD_SOURCE(src));
1100
+ g_assert_cmpint(ret, ==, 0);
1101
+ rdm = memory_region_get_ram_discard_manager(test_mr);
1102
+
1103
+ /* Register listener for exactly the last 4 blocks */
1104
+ test_listener_init(&tl);
1105
+ section.mr = test_mr;
1106
+ section.offset_within_region = last_offset;
1107
+ section.size = int128_make64(GRANULARITY_4K * 4);
1108
+ ram_discard_manager_register_listener(rdm, &tl.rdl, §ion);
1109
+
1110
+ /* Should receive notification for the populated range */
1111
+ g_assert_cmpint(tl.populate_count, ==, 1);
1112
+ g_assert_cmpuint(tl.last_populate_offset, ==, last_offset);
1113
+ g_assert_cmpuint(tl.last_populate_size, ==, GRANULARITY_4K * 4);
1114
+
1115
+ /* Discard exactly at boundary */
1116
+ tl.discard_count = 0;
1117
+ test_source_discard(src, last_offset, GRANULARITY_4K * 4);
1118
+ ram_discard_manager_notify_discard(rdm, RAM_DISCARD_SOURCE(src),
1119
+ last_offset, GRANULARITY_4K * 4);
1120
+ g_assert_cmpint(tl.discard_count, ==, 1);
1121
+
1122
+ ram_discard_manager_unregister_listener(rdm, &tl.rdl);
1123
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src));
1124
+ test_source_free(src);
1125
+ test_teardown();
1126
+}
1127
+
1128
+static int count_discarded_blocks(const MemoryRegionSection *section,
1129
+ void *opaque)
1130
+{
1131
+ int *count = opaque;
1132
+ *count += int128_get64(section->size) / GRANULARITY_4K;
1133
+ return 0;
1134
+}
1135
+
1136
+/*
1137
+ * Test: replay_discarded with two sources (union semantics).
1138
+ */
1139
+static void test_replay_discarded(void)
1140
+{
1141
+ TestRamDiscardSource *src1, *src2;
1142
+ RamDiscardManager *rdm;
1143
+ MemoryRegionSection section;
1144
+ int count = 0;
1145
+ int ret;
1146
+
1147
+ test_setup();
1148
+
1149
+ src1 = test_source_new(test_mr, GRANULARITY_4K);
1150
+ src2 = test_source_new(test_mr, GRANULARITY_4K);
1151
+
1152
+ /*
1153
+ * src1: blocks 0-3 populated, rest discarded
1154
+ * src2: blocks 2-5 populated, rest discarded
1155
+ * Aggregated populated: blocks 2-3 (intersection)
1156
+ * Aggregated discarded: blocks 0-1, 4-5, 6+ (union of discarded)
1157
+ */
1158
+ test_source_populate(src1, 0, GRANULARITY_4K * 4);
1159
+ test_source_populate(src2, GRANULARITY_4K * 2, GRANULARITY_4K * 4);
1160
+
1161
+ ret = memory_region_add_ram_discard_source(test_mr,
1162
+ RAM_DISCARD_SOURCE(src1));
1163
+ g_assert_cmpint(ret, ==, 0);
1164
+ ret = memory_region_add_ram_discard_source(test_mr,
1165
+ RAM_DISCARD_SOURCE(src2));
1166
+ g_assert_cmpint(ret, ==, 0);
1167
+
1168
+ rdm = memory_region_get_ram_discard_manager(test_mr);
1169
+
1170
+ section.mr = test_mr;
1171
+ section.offset_within_region = 0;
1172
+ section.size = int128_make64(GRANULARITY_4K * 8);
1173
+
1174
+ /* Count discarded blocks */
1175
+ ret = ram_discard_manager_replay_discarded(rdm, §ion,
1176
+ count_discarded_blocks, &count);
1177
+
1178
+ g_assert_cmpint(ret, ==, 0);
1179
+ /* Discarded: blocks 0-1 (2), blocks 4-5 (2), blocks 6-7 (2) = 6 blocks */
1180
+ g_assert_cmpint(count, ==, 6);
1181
+
1182
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2));
1183
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1));
1184
+
1185
+ test_source_free(src2);
1186
+ test_source_free(src1);
1187
+ test_teardown();
1188
+}
1189
+
1190
+int main(int argc, char **argv)
1191
+{
1192
+ g_test_init(&argc, &argv, NULL);
1193
+
1194
+ module_call_init(MODULE_INIT_QOM);
1195
+ type_register_static(&test_rds_info);
1196
+
1197
+ g_test_add_func("/ram-discard-manager/single-source/basic",
1198
+ test_single_source_basic);
1199
+ g_test_add_func("/ram-discard-manager/single-source/listener",
1200
+ test_single_source_listener);
1201
+ g_test_add_func("/ram-discard-manager/two-sources/same-granularity",
1202
+ test_two_sources_same_granularity);
1203
+ g_test_add_func("/ram-discard-manager/two-sources/different-granularity",
1204
+ test_two_sources_different_granularity);
1205
+ g_test_add_func("/ram-discard-manager/two-sources/notification",
1206
+ test_two_sources_notification);
1207
+ g_test_add_func("/ram-discard-manager/dynamic/add-source-with-listener",
1208
+ test_add_source_with_listener);
1209
+ g_test_add_func("/ram-discard-manager/dynamic/remove-source-with-listener",
1210
+ test_remove_source_with_listener);
1211
+ g_test_add_func("/ram-discard-manager/dynamic/readd-source-with-listener",
1212
+ test_readd_source_with_listener);
1213
+ g_test_add_func("/ram-discard-manager/edge/duplicate-source",
1214
+ test_duplicate_source);
1215
+ g_test_add_func("/ram-discard-manager/edge/populate-rollback",
1216
+ test_populate_rollback);
1217
+ g_test_add_func("/ram-discard-manager/edge/replay-intersection",
1218
+ test_replay_populated_intersection);
1219
+ g_test_add_func("/ram-discard-manager/edge/no-sources",
1220
+ test_no_sources);
1221
+ g_test_add_func("/ram-discard-manager/multi-source/redundant-discard",
1222
+ test_redundant_discard);
1223
+ g_test_add_func("/ram-discard-manager/listener/partial-section",
1224
+ test_partial_listener_section);
1225
+ g_test_add_func("/ram-discard-manager/listener/multiple-different",
1226
+ test_multiple_listeners_different_sections);
1227
+ g_test_add_func("/ram-discard-manager/listener/overlapping",
1228
+ test_overlapping_listener_sections);
1229
+ g_test_add_func("/ram-discard-manager/edge/boundary-section",
1230
+ test_boundary_section);
1231
+ g_test_add_func("/ram-discard-manager/multi-source/replay-discarded",
1232
+ test_replay_discarded);
1233
+
1234
+ return g_test_run();
1235
+}