/* SPDX-License-Identifier: GPL-2.0-or-later */ #include "qemu/osdep.h" #include "qemu/bitmap.h" #include "qemu/module.h" #include "qemu/main-loop.h" #include "qapi/error.h" #include "qom/object.h" #include "qom/qom-qobject.h" #include "glib.h" #include "system/memory.h" #define TYPE_TEST_RAM_DISCARD_SOURCE "test-ram-discard-source" OBJECT_DECLARE_SIMPLE_TYPE(TestRamDiscardSource, TEST_RAM_DISCARD_SOURCE) struct TestRamDiscardSource { Object parent; MemoryRegion *mr; uint64_t granularity; unsigned long *bitmap; uint64_t bitmap_size; }; static uint64_t test_rds_get_min_granularity(const RamDiscardSource *rds, const MemoryRegion *mr) { TestRamDiscardSource *src = TEST_RAM_DISCARD_SOURCE(rds); g_assert(mr == src->mr); return src->granularity; } static bool test_rds_is_populated(const RamDiscardSource *rds, const MemoryRegionSection *section) { TestRamDiscardSource *src = TEST_RAM_DISCARD_SOURCE(rds); uint64_t offset = section->offset_within_region; uint64_t size = int128_get64(section->size); uint64_t first_bit = offset / src->granularity; uint64_t last_bit = (offset + size - 1) / src->granularity; unsigned long found; g_assert(section->mr == src->mr); /* Check if any bit in range is zero (discarded) */ found = find_next_zero_bit(src->bitmap, last_bit + 1, first_bit); return found > last_bit; } static void test_rds_class_init(ObjectClass *klass, const void *data) { RamDiscardSourceClass *rdsc = RAM_DISCARD_SOURCE_CLASS(klass); rdsc->get_min_granularity = test_rds_get_min_granularity; rdsc->is_populated = test_rds_is_populated; } static const TypeInfo test_rds_info = { .name = TYPE_TEST_RAM_DISCARD_SOURCE, .parent = TYPE_OBJECT, .instance_size = sizeof(TestRamDiscardSource), .class_init = test_rds_class_init, .interfaces = (const InterfaceInfo[]) { { TYPE_RAM_DISCARD_SOURCE }, { } }, }; static TestRamDiscardSource *test_source_new(MemoryRegion *mr, uint64_t granularity) { TestRamDiscardSource *src; uint64_t region_size = memory_region_size(mr); src = TEST_RAM_DISCARD_SOURCE(object_new(TYPE_TEST_RAM_DISCARD_SOURCE)); src->mr = mr; src->granularity = granularity; src->bitmap_size = DIV_ROUND_UP(region_size, granularity); src->bitmap = bitmap_new(src->bitmap_size); return src; } static void test_source_free(TestRamDiscardSource *src) { g_free(src->bitmap); object_unref(OBJECT(src)); } static void test_source_populate(TestRamDiscardSource *src, uint64_t offset, uint64_t size) { uint64_t first_bit = offset / src->granularity; uint64_t nbits = size / src->granularity; bitmap_set(src->bitmap, first_bit, nbits); } static void test_source_discard(TestRamDiscardSource *src, uint64_t offset, uint64_t size) { uint64_t first_bit = offset / src->granularity; uint64_t nbits = size / src->granularity; bitmap_clear(src->bitmap, first_bit, nbits); } typedef struct TestListener { RamDiscardListener rdl; int populate_count; int discard_count; uint64_t last_populate_offset; uint64_t last_populate_size; uint64_t last_discard_offset; uint64_t last_discard_size; int fail_on_populate; /* Return error on Nth populate */ int populate_call_num; } TestListener; static int test_listener_populate(RamDiscardListener *rdl, const MemoryRegionSection *section) { TestListener *tl = container_of(rdl, TestListener, rdl); tl->populate_call_num++; if (tl->fail_on_populate > 0 && tl->populate_call_num >= tl->fail_on_populate) { return -ENOMEM; } tl->populate_count++; tl->last_populate_offset = section->offset_within_region; tl->last_populate_size = int128_get64(section->size); return 0; } static void test_listener_discard(RamDiscardListener *rdl, const MemoryRegionSection *section) { TestListener *tl = container_of(rdl, TestListener, rdl); tl->discard_count++; tl->last_discard_offset = section->offset_within_region; tl->last_discard_size = int128_get64(section->size); } static void test_listener_init(TestListener *tl) { ram_discard_listener_init(&tl->rdl, test_listener_populate, test_listener_discard); } #define TEST_REGION_SIZE (16 * 1024 * 1024) /* 16 MB */ #define GRANULARITY_4K (4 * 1024) #define GRANULARITY_2M (2 * 1024 * 1024) static MemoryRegion *test_mr; static void test_setup(void) { test_mr = g_new0(MemoryRegion, 1); test_mr->size = int128_make64(TEST_REGION_SIZE); test_mr->ram = true; } static void test_teardown(void) { g_clear_pointer(&test_mr->rdm, object_unref); object_unparent(OBJECT(test_mr)); g_free(test_mr); test_mr = NULL; } static void test_single_source_basic(void) { TestRamDiscardSource *src; RamDiscardManager *rdm; MemoryRegionSection section; int ret; test_setup(); src = test_source_new(test_mr, GRANULARITY_4K); rdm = memory_region_get_ram_discard_manager(test_mr); g_assert_null(rdm); /* Add source */ ret = memory_region_add_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src)); g_assert_cmpint(ret, ==, 0); rdm = memory_region_get_ram_discard_manager(test_mr); g_assert_nonnull(rdm); g_assert_cmpuint(ram_discard_manager_get_min_granularity(rdm, test_mr), ==, GRANULARITY_4K); /* Initially all discarded */ section.mr = test_mr; section.offset_within_region = 0; section.size = int128_make64(GRANULARITY_4K); g_assert_false(ram_discard_manager_is_populated(rdm, §ion)); /* Populate a range in source */ test_source_populate(src, 0, GRANULARITY_4K * 4); /* Now should be populated */ g_assert_true(ram_discard_manager_is_populated(rdm, §ion)); /* Check larger section */ section.size = int128_make64(GRANULARITY_4K * 4); g_assert_true(ram_discard_manager_is_populated(rdm, §ion)); /* Check section that spans populated and discarded */ section.size = int128_make64(GRANULARITY_4K * 8); g_assert_false(ram_discard_manager_is_populated(rdm, §ion)); memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src)); g_assert_true(ram_discard_manager_is_populated(rdm, §ion)); test_source_free(src); test_teardown(); } static void test_single_source_listener(void) { TestRamDiscardSource *src; RamDiscardManager *rdm; MemoryRegionSection section; TestListener tl = { 0, }; int ret; test_setup(); src = test_source_new(test_mr, GRANULARITY_4K); /* Populate some ranges before adding listener */ test_source_populate(src, 0, GRANULARITY_4K * 4); test_source_populate(src, GRANULARITY_4K * 8, GRANULARITY_4K * 4); ret = memory_region_add_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src)); g_assert_cmpint(ret, ==, 0); rdm = memory_region_get_ram_discard_manager(test_mr); g_assert_nonnull(rdm); /* Register listener */ test_listener_init(&tl); section.mr = test_mr; section.offset_within_region = 0; section.size = int128_make64(TEST_REGION_SIZE); ram_discard_manager_register_listener(rdm, &tl.rdl, §ion); /* Should have been notified about populated regions */ g_assert_cmpint(tl.populate_count, ==, 2); /* Notify populate for new range */ tl.populate_count = 0; test_source_populate(src, GRANULARITY_4K * 16, GRANULARITY_4K * 2); ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src), GRANULARITY_4K * 16, GRANULARITY_4K * 2); g_assert_cmpint(ret, ==, 0); g_assert_cmpint(tl.populate_count, ==, 1); g_assert_cmpuint(tl.last_populate_offset, ==, GRANULARITY_4K * 16); g_assert_cmpuint(tl.last_populate_size, ==, GRANULARITY_4K * 2); /* Notify discard */ tl.discard_count = 0; test_source_discard(src, 0, GRANULARITY_4K * 4); ram_discard_manager_notify_discard(rdm, RAM_DISCARD_SOURCE(src), 0, GRANULARITY_4K * 4); g_assert_cmpint(tl.discard_count, ==, 1); g_assert_cmpuint(tl.last_discard_offset, ==, 0); g_assert_cmpuint(tl.last_discard_size, ==, GRANULARITY_4K * 4); /* Unregister listener */ ram_discard_manager_unregister_listener(rdm, &tl.rdl); memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src)); test_source_free(src); test_teardown(); } static void test_two_sources_same_granularity(void) { TestRamDiscardSource *src1, *src2; RamDiscardManager *rdm; MemoryRegionSection section; int ret; test_setup(); src1 = test_source_new(test_mr, GRANULARITY_4K); src2 = test_source_new(test_mr, GRANULARITY_4K); /* Add first source */ ret = memory_region_add_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1)); g_assert_cmpint(ret, ==, 0); /* Add second source */ ret = memory_region_add_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2)); g_assert_cmpint(ret, ==, 0); rdm = memory_region_get_ram_discard_manager(test_mr); g_assert_nonnull(rdm); /* Check granularity */ g_assert_cmpuint(ram_discard_manager_get_min_granularity(rdm, test_mr), ==, GRANULARITY_4K); section.mr = test_mr; section.offset_within_region = 0; section.size = int128_make64(GRANULARITY_4K); /* Both discarded -> aggregated discarded */ g_assert_false(ram_discard_manager_is_populated(rdm, §ion)); /* Populate in src1 only */ test_source_populate(src1, 0, GRANULARITY_4K); g_assert_false(ram_discard_manager_is_populated(rdm, §ion)); /* Populate in src2 only */ test_source_discard(src1, 0, GRANULARITY_4K); test_source_populate(src2, 0, GRANULARITY_4K); g_assert_false(ram_discard_manager_is_populated(rdm, §ion)); /* Populate in both -> aggregated populated */ test_source_populate(src1, 0, GRANULARITY_4K); g_assert_true(ram_discard_manager_is_populated(rdm, §ion)); /* Remove sources */ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2)); memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1)); test_source_free(src2); test_source_free(src1); test_teardown(); } /* * Test: Two sources with different granularities (4K and 2M). * The aggregated granularity should be GCD(4K, 2M) = 4K. */ static void test_two_sources_different_granularity(void) { TestRamDiscardSource *src_4k, *src_2m; RamDiscardManager *rdm; MemoryRegionSection section; int ret; test_setup(); src_4k = test_source_new(test_mr, GRANULARITY_4K); src_2m = test_source_new(test_mr, GRANULARITY_2M); ret = memory_region_add_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src_4k)); g_assert_cmpint(ret, ==, 0); ret = memory_region_add_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src_2m)); g_assert_cmpint(ret, ==, 0); rdm = memory_region_get_ram_discard_manager(test_mr); g_assert_cmpuint(ram_discard_manager_get_min_granularity(rdm, test_mr), ==, GRANULARITY_4K); section.mr = test_mr; section.offset_within_region = 0; section.size = int128_make64(GRANULARITY_4K); /* Both discarded */ g_assert_false(ram_discard_manager_is_populated(rdm, §ion)); /* Populate 4K in src_4k, but src_2m still discarded the whole 2M block */ test_source_populate(src_4k, 0, GRANULARITY_4K); g_assert_false(ram_discard_manager_is_populated(rdm, §ion)); /* Populate 2M in src_2m (which includes the 4K block) */ test_source_populate(src_2m, 0, GRANULARITY_2M); g_assert_true(ram_discard_manager_is_populated(rdm, §ion)); /* Check a 4K block at offset 4K (populated in src_2m but not in src_4k) */ section.offset_within_region = GRANULARITY_4K; g_assert_false(ram_discard_manager_is_populated(rdm, §ion)); /* Populate it in src_4k */ test_source_populate(src_4k, GRANULARITY_4K, GRANULARITY_4K); g_assert_true(ram_discard_manager_is_populated(rdm, §ion)); memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src_2m)); memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src_4k)); test_source_free(src_2m); test_source_free(src_4k); test_teardown(); } /* * Test: Notification with two sources. * Populate notification should only fire when all sources are populated. */ static void test_two_sources_notification(void) { TestRamDiscardSource *src1, *src2; RamDiscardManager *rdm; MemoryRegionSection section; TestListener tl = { 0, }; int ret; test_setup(); src1 = test_source_new(test_mr, GRANULARITY_4K); src2 = test_source_new(test_mr, GRANULARITY_4K); ret = memory_region_add_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1)); g_assert_cmpint(ret, ==, 0); ret = memory_region_add_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2)); g_assert_cmpint(ret, ==, 0); rdm = memory_region_get_ram_discard_manager(test_mr); /* Register listener */ test_listener_init(&tl); section.mr = test_mr; section.offset_within_region = 0; section.size = int128_make64(TEST_REGION_SIZE); ram_discard_manager_register_listener(rdm, &tl.rdl, §ion); /* No populate notifications yet (all discarded) */ g_assert_cmpint(tl.populate_count, ==, 0); /* Populate in src1 only - no notification (src2 still discarded) */ test_source_populate(src1, 0, GRANULARITY_4K * 4); ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src1), 0, GRANULARITY_4K * 4); g_assert_cmpint(ret, ==, 0); g_assert_cmpint(tl.populate_count, ==, 0); /* Populate same range in src2 - now should notify */ test_source_populate(src2, 0, GRANULARITY_4K * 4); ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src2), 0, GRANULARITY_4K * 4); g_assert_cmpint(ret, ==, 0); g_assert_cmpint(tl.populate_count, ==, 1); /* Discard from src1 - should notify discard immediately */ tl.discard_count = 0; test_source_discard(src1, 0, GRANULARITY_4K * 2); ram_discard_manager_notify_discard(rdm, RAM_DISCARD_SOURCE(src1), 0, GRANULARITY_4K * 2); g_assert_cmpint(tl.discard_count, ==, 1); ram_discard_manager_unregister_listener(rdm, &tl.rdl); memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2)); memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1)); test_source_free(src2); test_source_free(src1); test_teardown(); } /* * Test: Adding source with existing listener. * When a new source is added, listeners should be notified about * regions that become discarded. */ static void test_add_source_with_listener(void) { TestRamDiscardSource *src1, *src2; RamDiscardManager *rdm; MemoryRegionSection section; TestListener tl = { 0, }; int ret; test_setup(); src1 = test_source_new(test_mr, GRANULARITY_4K); src2 = test_source_new(test_mr, GRANULARITY_4K); /* Populate some range in src1 */ test_source_populate(src1, 0, GRANULARITY_4K * 8); ret = memory_region_add_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1)); g_assert_cmpint(ret, ==, 0); rdm = memory_region_get_ram_discard_manager(test_mr); /* Register listener */ test_listener_init(&tl); section.mr = test_mr; section.offset_within_region = 0; section.size = int128_make64(TEST_REGION_SIZE); ram_discard_manager_register_listener(rdm, &tl.rdl, §ion); /* Should have been notified about populated region */ g_assert_cmpint(tl.populate_count, ==, 1); g_assert_cmpint(tl.last_populate_offset, ==, 0); g_assert_cmpint(tl.last_populate_size, ==, GRANULARITY_4K * 8); /* src2 has part of the region populated, part discarded */ /* src2 has 0-4 populated, 4-8 discarded */ test_source_populate(src2, 0, GRANULARITY_4K * 4); /* Add src2 - listener should be notified about newly discarded regions */ tl.discard_count = 0; ret = memory_region_add_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2)); g_assert_cmpint(ret, ==, 0); /* * The range 4K*4 to 4K*8 was populated in src1 but discarded in src2, * so it becomes aggregated-discarded. Listener should be notified. * Only this range should trigger a discard notification - regions beyond * 4K*8 were already discarded in src1, so adding src2 doesn't change them. */ g_assert_cmpint(tl.discard_count, ==, 1); g_assert_cmpint(tl.last_discard_offset, ==, GRANULARITY_4K * 4); g_assert_cmpint(tl.last_discard_size, ==, GRANULARITY_4K * 4); ram_discard_manager_unregister_listener(rdm, &tl.rdl); memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2)); memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1)); test_source_free(src2); test_source_free(src1); test_teardown(); } /* * Test: Removing source with existing listener. * When a source is removed, listeners should be notified about * regions that become populated. */ static void test_remove_source_with_listener(void) { TestRamDiscardSource *src1, *src2; RamDiscardManager *rdm; MemoryRegionSection section; TestListener tl = { 0, }; int ret; test_setup(); src1 = test_source_new(test_mr, GRANULARITY_4K); src2 = test_source_new(test_mr, GRANULARITY_4K); /* src1: all of first 8 blocks populated */ test_source_populate(src1, 0, GRANULARITY_4K * 8); /* src2: only first 4 blocks populated */ test_source_populate(src2, 0, GRANULARITY_4K * 4); ret = memory_region_add_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1)); g_assert_cmpint(ret, ==, 0); ret = memory_region_add_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2)); g_assert_cmpint(ret, ==, 0); rdm = memory_region_get_ram_discard_manager(test_mr); /* Register listener */ test_listener_init(&tl); section.mr = test_mr; section.offset_within_region = 0; section.size = int128_make64(TEST_REGION_SIZE); ram_discard_manager_register_listener(rdm, &tl.rdl, §ion); /* Only first 4 blocks are aggregated-populated */ g_assert_cmpint(tl.populate_count, ==, 1); g_assert_cmpuint(tl.last_populate_size, ==, GRANULARITY_4K * 4); /* Remove src2 - blocks 4-8 should become populated */ tl.populate_count = 0; memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2)); /* Listener should be notified about newly populated region (4K*4 to 4K*8) */ g_assert_cmpint(tl.populate_count, >=, 1); ram_discard_manager_unregister_listener(rdm, &tl.rdl); memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1)); test_source_free(src2); test_source_free(src1); test_teardown(); } /* * Test: Add a source, register a listener, remove the source, then add it back. * This checks the transition from 0 sources (all populated) to 1 source * (partially discarded) with an active listener. */ static void test_readd_source_with_listener(void) { TestRamDiscardSource *src; RamDiscardManager *rdm; MemoryRegionSection section; TestListener tl = { 0, }; int ret; test_setup(); src = test_source_new(test_mr, GRANULARITY_4K); /* Populate some range in src */ test_source_populate(src, 0, GRANULARITY_4K * 8); /* 1. Add source */ ret = memory_region_add_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src)); g_assert_cmpint(ret, ==, 0); rdm = memory_region_get_ram_discard_manager(test_mr); /* 2. Register listener */ test_listener_init(&tl); section.mr = test_mr; section.offset_within_region = 0; section.size = int128_make64(TEST_REGION_SIZE); ram_discard_manager_register_listener(rdm, &tl.rdl, §ion); /* Listener notified about populated region (0 - 32K) */ g_assert_cmpint(tl.populate_count, ==, 1); g_assert_cmpuint(tl.last_populate_size, ==, GRANULARITY_4K * 8); /* 3. Remove source */ tl.populate_count = 0; memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src)); /* * With 0 sources, everything is populated. * The range that was discarded in src (from 32K to end) becomes populated. */ g_assert_cmpint(tl.populate_count, ==, 1); g_assert_cmpuint(tl.last_populate_offset, ==, GRANULARITY_4K * 8); g_assert_cmpuint(tl.last_populate_size, ==, TEST_REGION_SIZE - GRANULARITY_4K * 8); /* 4. Add source back */ tl.discard_count = 0; ret = memory_region_add_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src)); g_assert_cmpint(ret, ==, 0); /* * Now we have 1 source again. The range (32K to end) is discarded again. * Listener should be notified about this discard. */ g_assert_cmpint(tl.discard_count, ==, 1); g_assert_cmpuint(tl.last_discard_offset, ==, GRANULARITY_4K * 8); g_assert_cmpuint(tl.last_discard_size, ==, TEST_REGION_SIZE - GRANULARITY_4K * 8); ram_discard_manager_unregister_listener(rdm, &tl.rdl); memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src)); test_source_free(src); test_teardown(); } /* * Test: Duplicate source registration should fail. */ static void test_duplicate_source(void) { TestRamDiscardSource *src; int ret; test_setup(); src = test_source_new(test_mr, GRANULARITY_4K); ret = memory_region_add_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src)); g_assert_cmpint(ret, ==, 0); /* Adding same source again should fail */ ret = memory_region_add_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src)); g_assert_cmpint(ret, ==, -EBUSY); memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src)); test_source_free(src); test_teardown(); } /* * Test: Populate notification rollback on listener error. */ static void test_populate_rollback(void) { TestRamDiscardSource *src; RamDiscardManager *rdm; MemoryRegionSection section; TestListener tl1 = { 0, }, tl2 = { 0, }; int ret; test_setup(); src = test_source_new(test_mr, GRANULARITY_4K); ret = memory_region_add_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src)); g_assert_cmpint(ret, ==, 0); rdm = memory_region_get_ram_discard_manager(test_mr); /* Register two listeners */ test_listener_init(&tl1); test_listener_init(&tl2); tl2.fail_on_populate = 1; /* Second listener fails on first populate */ section.mr = test_mr; section.offset_within_region = 0; section.size = int128_make64(TEST_REGION_SIZE); /* * Register tl2 first so it's visited second (QLIST_INSERT_HEAD reverses * registration order). This ensures tl1 receives populate before tl2 * fails. */ ram_discard_manager_register_listener(rdm, &tl2.rdl, §ion); ram_discard_manager_register_listener(rdm, &tl1.rdl, §ion); /* Try to populate - should fail and roll back */ test_source_populate(src, 0, GRANULARITY_4K); ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src), 0, GRANULARITY_4K); g_assert_cmpint(ret, ==, -ENOMEM); /* First listener should have received populate then discard (rollback) */ g_assert_cmpint(tl1.populate_count, ==, 1); g_assert_cmpint(tl1.discard_count, ==, 1); ram_discard_manager_unregister_listener(rdm, &tl1.rdl); ram_discard_manager_unregister_listener(rdm, &tl2.rdl); memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src)); test_source_free(src); test_teardown(); } /* * Test: Replay populated with two sources (intersection). */ static void test_replay_populated_intersection(void) { TestRamDiscardSource *src1, *src2; RamDiscardManager *rdm; MemoryRegionSection section; TestListener tl = { 0, }; int ret; test_setup(); src1 = test_source_new(test_mr, GRANULARITY_4K); src2 = test_source_new(test_mr, GRANULARITY_4K); /* * src1: blocks 0-7 populated * src2: blocks 4-11 populated * Intersection: blocks 4-7 */ test_source_populate(src1, 0, GRANULARITY_4K * 8); test_source_populate(src2, GRANULARITY_4K * 4, GRANULARITY_4K * 8); ret = memory_region_add_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1)); g_assert_cmpint(ret, ==, 0); ret = memory_region_add_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2)); g_assert_cmpint(ret, ==, 0); rdm = memory_region_get_ram_discard_manager(test_mr); /* Register listener - should only get notified about intersection */ test_listener_init(&tl); section.mr = test_mr; section.offset_within_region = 0; section.size = int128_make64(TEST_REGION_SIZE); ram_discard_manager_register_listener(rdm, &tl.rdl, §ion); /* Should have been notified about blocks 4-7 (intersection) */ g_assert_cmpint(tl.populate_count, ==, 1); g_assert_cmpuint(tl.last_populate_offset, ==, GRANULARITY_4K * 4); g_assert_cmpuint(tl.last_populate_size, ==, GRANULARITY_4K * 4); ram_discard_manager_unregister_listener(rdm, &tl.rdl); memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2)); memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1)); test_source_free(src2); test_source_free(src1); test_teardown(); } /* * Test: Empty region (no sources). */ static void test_no_sources(void) { test_setup(); /* No sources - should have no manager */ g_assert_null(memory_region_get_ram_discard_manager(test_mr)); g_assert_false(memory_region_has_ram_discard_manager(test_mr)); test_teardown(); } static void test_redundant_discard(void) { TestRamDiscardSource *src1, *src2; RamDiscardManager *rdm; MemoryRegionSection section; TestListener tl = { 0, }; int ret; test_setup(); src1 = test_source_new(test_mr, GRANULARITY_4K); src2 = test_source_new(test_mr, GRANULARITY_4K); /* Add sources */ ret = memory_region_add_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1)); g_assert_cmpint(ret, ==, 0); ret = memory_region_add_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2)); g_assert_cmpint(ret, ==, 0); rdm = memory_region_get_ram_discard_manager(test_mr); /* Register listener */ test_listener_init(&tl); section.mr = test_mr; section.offset_within_region = 0; section.size = int128_make64(TEST_REGION_SIZE); ram_discard_manager_register_listener(rdm, &tl.rdl, §ion); /* Populate intersection (0-4K) in both sources */ test_source_populate(src1, 0, GRANULARITY_4K); test_source_populate(src2, 0, GRANULARITY_4K); /* Notify populate src1 - should trigger listener populate */ ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src1), 0, GRANULARITY_4K); g_assert_cmpint(ret, ==, 0); g_assert_cmpint(tl.populate_count, ==, 1); /* Now Discard src1 -> Aggregate Discarded */ tl.discard_count = 0; test_source_discard(src1, 0, GRANULARITY_4K); ram_discard_manager_notify_discard(rdm, RAM_DISCARD_SOURCE(src1), 0, GRANULARITY_4K); g_assert_cmpint(tl.discard_count, ==, 1); /* Now Discard src2 -> Aggregate Discarded (Already Discarded!) */ /* Listener should NOT receive another discard notification for the same range. */ test_source_discard(src2, 0, GRANULARITY_4K); ram_discard_manager_notify_discard(rdm, RAM_DISCARD_SOURCE(src2), 0, GRANULARITY_4K); g_assert_cmpint(tl.discard_count, ==, 1); ram_discard_manager_unregister_listener(rdm, &tl.rdl); memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2)); memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1)); test_source_free(src2); test_source_free(src1); test_teardown(); } /* * Test: Listener with partial section coverage. * Listener should only receive notifications for its registered range. */ static void test_partial_listener_section(void) { TestRamDiscardSource *src; RamDiscardManager *rdm; MemoryRegionSection section; TestListener tl = { 0, }; int ret; test_setup(); src = test_source_new(test_mr, GRANULARITY_4K); /* Populate blocks 0-7 */ test_source_populate(src, 0, GRANULARITY_4K * 8); ret = memory_region_add_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src)); g_assert_cmpint(ret, ==, 0); rdm = memory_region_get_ram_discard_manager(test_mr); /* Register listener for only blocks 2-5 (not the full region) */ test_listener_init(&tl); section.mr = test_mr; section.offset_within_region = GRANULARITY_4K * 2; section.size = int128_make64(GRANULARITY_4K * 4); ram_discard_manager_register_listener(rdm, &tl.rdl, §ion); /* Should be notified only about blocks 2-5 (intersection) */ g_assert_cmpint(tl.populate_count, ==, 1); g_assert_cmpuint(tl.last_populate_offset, ==, GRANULARITY_4K * 2); g_assert_cmpuint(tl.last_populate_size, ==, GRANULARITY_4K * 4); /* Discard block 0 - outside listener's section, no notification */ tl.discard_count = 0; test_source_discard(src, 0, GRANULARITY_4K); ram_discard_manager_notify_discard(rdm, RAM_DISCARD_SOURCE(src), 0, GRANULARITY_4K); g_assert_cmpint(tl.discard_count, ==, 0); /* Discard block 3 - inside listener's section */ test_source_discard(src, GRANULARITY_4K * 3, GRANULARITY_4K); ram_discard_manager_notify_discard(rdm, RAM_DISCARD_SOURCE(src), GRANULARITY_4K * 3, GRANULARITY_4K); g_assert_cmpint(tl.discard_count, ==, 1); g_assert_cmpuint(tl.last_discard_offset, ==, GRANULARITY_4K * 3); /* Discard spanning boundary (blocks 5-6) - only block 5 in section */ tl.discard_count = 0; test_source_discard(src, GRANULARITY_4K * 5, GRANULARITY_4K * 2); ram_discard_manager_notify_discard(rdm, RAM_DISCARD_SOURCE(src), GRANULARITY_4K * 5, GRANULARITY_4K * 2); g_assert_cmpint(tl.discard_count, ==, 1); g_assert_cmpuint(tl.last_discard_offset, ==, GRANULARITY_4K * 5); g_assert_cmpuint(tl.last_discard_size, ==, GRANULARITY_4K); ram_discard_manager_unregister_listener(rdm, &tl.rdl); memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src)); test_source_free(src); test_teardown(); } /* * Test: Multiple listeners with different (non-overlapping) sections. */ static void test_multiple_listeners_different_sections(void) { TestRamDiscardSource *src; RamDiscardManager *rdm; MemoryRegionSection section1, section2; TestListener tl1 = { 0, }, tl2 = { 0, }; int ret; test_setup(); src = test_source_new(test_mr, GRANULARITY_4K); ret = memory_region_add_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src)); g_assert_cmpint(ret, ==, 0); rdm = memory_region_get_ram_discard_manager(test_mr); /* Listener 1: blocks 0-3 */ test_listener_init(&tl1); section1.mr = test_mr; section1.offset_within_region = 0; section1.size = int128_make64(GRANULARITY_4K * 4); ram_discard_manager_register_listener(rdm, &tl1.rdl, §ion1); /* Listener 2: blocks 8-11 */ test_listener_init(&tl2); section2.mr = test_mr; section2.offset_within_region = GRANULARITY_4K * 8; section2.size = int128_make64(GRANULARITY_4K * 4); ram_discard_manager_register_listener(rdm, &tl2.rdl, §ion2); /* Initially all discarded - no populate notifications */ g_assert_cmpint(tl1.populate_count, ==, 0); g_assert_cmpint(tl2.populate_count, ==, 0); /* Populate blocks 0-3 - only tl1 should be notified */ test_source_populate(src, 0, GRANULARITY_4K * 4); ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src), 0, GRANULARITY_4K * 4); g_assert_cmpint(ret, ==, 0); g_assert_cmpint(tl1.populate_count, ==, 1); g_assert_cmpint(tl2.populate_count, ==, 0); /* Populate blocks 8-11 - only tl2 should be notified */ test_source_populate(src, GRANULARITY_4K * 8, GRANULARITY_4K * 4); ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src), GRANULARITY_4K * 8, GRANULARITY_4K * 4); g_assert_cmpint(ret, ==, 0); g_assert_cmpint(tl1.populate_count, ==, 1); g_assert_cmpint(tl2.populate_count, ==, 1); /* Populate blocks 4-7 (gap) - neither listener should be notified */ test_source_populate(src, GRANULARITY_4K * 4, GRANULARITY_4K * 4); ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src), GRANULARITY_4K * 4, GRANULARITY_4K * 4); g_assert_cmpint(ret, ==, 0); g_assert_cmpint(tl1.populate_count, ==, 1); g_assert_cmpint(tl2.populate_count, ==, 1); ram_discard_manager_unregister_listener(rdm, &tl2.rdl); ram_discard_manager_unregister_listener(rdm, &tl1.rdl); memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src)); test_source_free(src); test_teardown(); } /* * Test: Multiple listeners with overlapping sections. */ static void test_overlapping_listener_sections(void) { TestRamDiscardSource *src; RamDiscardManager *rdm; MemoryRegionSection section1, section2; TestListener tl1 = { 0, }, tl2 = { 0, }; int ret; test_setup(); src = test_source_new(test_mr, GRANULARITY_4K); ret = memory_region_add_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src)); g_assert_cmpint(ret, ==, 0); rdm = memory_region_get_ram_discard_manager(test_mr); /* Listener 1: blocks 0-7 */ test_listener_init(&tl1); section1.mr = test_mr; section1.offset_within_region = 0; section1.size = int128_make64(GRANULARITY_4K * 8); ram_discard_manager_register_listener(rdm, &tl1.rdl, §ion1); /* Listener 2: blocks 4-11 (overlaps with tl1 on blocks 4-7) */ test_listener_init(&tl2); section2.mr = test_mr; section2.offset_within_region = GRANULARITY_4K * 4; section2.size = int128_make64(GRANULARITY_4K * 8); ram_discard_manager_register_listener(rdm, &tl2.rdl, §ion2); /* Populate blocks 4-7 (overlap region) - both should be notified */ test_source_populate(src, GRANULARITY_4K * 4, GRANULARITY_4K * 4); ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src), GRANULARITY_4K * 4, GRANULARITY_4K * 4); g_assert_cmpint(ret, ==, 0); g_assert_cmpint(tl1.populate_count, ==, 1); g_assert_cmpint(tl2.populate_count, ==, 1); /* Populate blocks 0-3 - only tl1 */ test_source_populate(src, 0, GRANULARITY_4K * 4); ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src), 0, GRANULARITY_4K * 4); g_assert_cmpint(ret, ==, 0); g_assert_cmpint(tl1.populate_count, ==, 2); g_assert_cmpint(tl2.populate_count, ==, 1); /* Populate blocks 8-11 - only tl2 */ test_source_populate(src, GRANULARITY_4K * 8, GRANULARITY_4K * 4); ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src), GRANULARITY_4K * 8, GRANULARITY_4K * 4); g_assert_cmpint(ret, ==, 0); g_assert_cmpint(tl1.populate_count, ==, 2); g_assert_cmpint(tl2.populate_count, ==, 2); ram_discard_manager_unregister_listener(rdm, &tl2.rdl); ram_discard_manager_unregister_listener(rdm, &tl1.rdl); memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src)); test_source_free(src); test_teardown(); } /* * Test: Listener at exact memory region boundaries. */ static void test_boundary_section(void) { TestRamDiscardSource *src; RamDiscardManager *rdm; MemoryRegionSection section; TestListener tl = { 0, }; uint64_t last_offset; int ret; test_setup(); src = test_source_new(test_mr, GRANULARITY_4K); /* Populate last 4 blocks of the region */ last_offset = TEST_REGION_SIZE - GRANULARITY_4K * 4; test_source_populate(src, last_offset, GRANULARITY_4K * 4); ret = memory_region_add_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src)); g_assert_cmpint(ret, ==, 0); rdm = memory_region_get_ram_discard_manager(test_mr); /* Register listener for exactly the last 4 blocks */ test_listener_init(&tl); section.mr = test_mr; section.offset_within_region = last_offset; section.size = int128_make64(GRANULARITY_4K * 4); ram_discard_manager_register_listener(rdm, &tl.rdl, §ion); /* Should receive notification for the populated range */ g_assert_cmpint(tl.populate_count, ==, 1); g_assert_cmpuint(tl.last_populate_offset, ==, last_offset); g_assert_cmpuint(tl.last_populate_size, ==, GRANULARITY_4K * 4); /* Discard exactly at boundary */ tl.discard_count = 0; test_source_discard(src, last_offset, GRANULARITY_4K * 4); ram_discard_manager_notify_discard(rdm, RAM_DISCARD_SOURCE(src), last_offset, GRANULARITY_4K * 4); g_assert_cmpint(tl.discard_count, ==, 1); ram_discard_manager_unregister_listener(rdm, &tl.rdl); memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src)); test_source_free(src); test_teardown(); } static int count_discarded_blocks(const MemoryRegionSection *section, void *opaque) { int *count = opaque; *count += int128_get64(section->size) / GRANULARITY_4K; return 0; } /* * Test: replay_discarded with two sources (union semantics). */ static void test_replay_discarded(void) { TestRamDiscardSource *src1, *src2; RamDiscardManager *rdm; MemoryRegionSection section; int count = 0; int ret; test_setup(); src1 = test_source_new(test_mr, GRANULARITY_4K); src2 = test_source_new(test_mr, GRANULARITY_4K); /* * src1: blocks 0-3 populated, rest discarded * src2: blocks 2-5 populated, rest discarded * Aggregated populated: blocks 2-3 (intersection) * Aggregated discarded: blocks 0-1, 4-5, 6+ (union of discarded) */ test_source_populate(src1, 0, GRANULARITY_4K * 4); test_source_populate(src2, GRANULARITY_4K * 2, GRANULARITY_4K * 4); ret = memory_region_add_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1)); g_assert_cmpint(ret, ==, 0); ret = memory_region_add_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2)); g_assert_cmpint(ret, ==, 0); rdm = memory_region_get_ram_discard_manager(test_mr); section.mr = test_mr; section.offset_within_region = 0; section.size = int128_make64(GRANULARITY_4K * 8); /* Count discarded blocks */ ret = ram_discard_manager_replay_discarded(rdm, §ion, count_discarded_blocks, &count); g_assert_cmpint(ret, ==, 0); /* Discarded: blocks 0-1 (2), blocks 4-5 (2), blocks 6-7 (2) = 6 blocks */ g_assert_cmpint(count, ==, 6); memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2)); memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1)); test_source_free(src2); test_source_free(src1); test_teardown(); } int main(int argc, char **argv) { g_test_init(&argc, &argv, NULL); module_call_init(MODULE_INIT_QOM); type_register_static(&test_rds_info); g_test_add_func("/ram-discard-manager/single-source/basic", test_single_source_basic); g_test_add_func("/ram-discard-manager/single-source/listener", test_single_source_listener); g_test_add_func("/ram-discard-manager/two-sources/same-granularity", test_two_sources_same_granularity); g_test_add_func("/ram-discard-manager/two-sources/different-granularity", test_two_sources_different_granularity); g_test_add_func("/ram-discard-manager/two-sources/notification", test_two_sources_notification); g_test_add_func("/ram-discard-manager/dynamic/add-source-with-listener", test_add_source_with_listener); g_test_add_func("/ram-discard-manager/dynamic/remove-source-with-listener", test_remove_source_with_listener); g_test_add_func("/ram-discard-manager/dynamic/readd-source-with-listener", test_readd_source_with_listener); g_test_add_func("/ram-discard-manager/edge/duplicate-source", test_duplicate_source); g_test_add_func("/ram-discard-manager/edge/populate-rollback", test_populate_rollback); g_test_add_func("/ram-discard-manager/edge/replay-intersection", test_replay_populated_intersection); g_test_add_func("/ram-discard-manager/edge/no-sources", test_no_sources); g_test_add_func("/ram-discard-manager/multi-source/redundant-discard", test_redundant_discard); g_test_add_func("/ram-discard-manager/listener/partial-section", test_partial_listener_section); g_test_add_func("/ram-discard-manager/listener/multiple-different", test_multiple_listeners_different_sections); g_test_add_func("/ram-discard-manager/listener/overlapping", test_overlapping_listener_sections); g_test_add_func("/ram-discard-manager/edge/boundary-section", test_boundary_section); g_test_add_func("/ram-discard-manager/multi-source/replay-discarded", test_replay_discarded); return g_test_run(); }