[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[PATCH v3 2/2] tests/native: Add native tests for NUMA claim sets



Test the page allocator's claim behaviour using:

- the legacy xc_domain_claim_pages() call, and
- the new xc_domain_claim_memory() call of the claim-set API,

exercising both host-wide and NUMA node-specific multi-node claims.

Signed-off-by: Bernhard Kaindl <bernhard.kaindl@xxxxxxxxxx>
---
 tools/tests/native/host-claims.c | 248 +++++++++++++++++++++++++++++++
 tools/tests/native/node-claims.c | 230 ++++++++++++++++++++++++++++
 2 files changed, 478 insertions(+)
 create mode 100644 tools/tests/native/host-claims.c
 create mode 100644 tools/tests/native/node-claims.c

diff --git a/tools/tests/native/host-claims.c b/tools/tests/native/host-claims.c
new file mode 100644
index 000000000000..8286fc586282
--- /dev/null
+++ b/tools/tests/native/host-claims.c
@@ -0,0 +1,248 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Test basic host-wide functionality of memory claims, including
+ * installing and redeeming claims, and that claims are respected
+ * by allocations and protected against other allocations.
+ *
+ * Copyright (C) 2026 Cloud Software Group
+ */
+
+#define TEST_ENABLE_XC_DOMAIN_C /* Enable xc_domain.c APIs */
+#include "harness/native.h"
+
+typedef int (*set_global_claims)(struct domain *d, unsigned long pages);
+set_global_claims install_host_claims;
+
+/* Install a host-wide claim using the legacy xc_domain_claim_pages() call */
+int install_host_claims_legacy(struct domain *d, unsigned long pages)
+{
+    if (pages == 0)
+        return xc_domain_claim_pages(xch, d->domain_id, 0);
+
+    /* The legacy call need resetting claims before claims can be set again */
+    xc_domain_claim_pages(xch, d->domain_id, 0);
+
+    /* The argument of the legacy call includes the domain's existing pages */
+    pages += domain_tot_pages(d);
+
+    return xc_domain_claim_pages(xch, d->domain_id, pages);
+}
+
+/* Install a host-wide claim set using the xc_domain.c hypercall API */
+int xc_domain_claim_memory_host(struct domain *d, unsigned long pages)
+{
+    xen_memory_claim_t claim_set[] = {
+        { .target = XEN_DOMCTL_CLAIM_MEMORY_HOST, .pages = pages },
+    };
+    uint32_t nr_entries = ARRAY_SIZE(claim_set);
+
+    return xc_domain_claim_memory(xch, d->domain_id,
+        XEN_DOMCTL_CLAIM_MEMORY_SET, &nr_entries, claim_set);
+}
+
+static void test_alloc_domheap_redeems_claims(int start_mfn)
+{
+    int ret;
+    struct page_info *pages = frame_table + start_mfn, *pg;
+
+    test_page_list_add_buddy(pages, order2);
+    ASSERT(!install_host_claims(dom1, 3));
+    ASSERT(alloc_domheap_pages(dom1, order1, 0) == pages + 2);
+    ASSERT(alloc_domheap_pages(dom1, order0, 0) == pages + 1);
+    CHECK(TOTAL_CLAIMS == 0, "Expect all claims consumed after allocations");
+    CHECK(FREE_PAGES == 1, "Expect one free page after allocations");
+
+    ASSERT(!install_host_claims(dom2, FREE_PAGES));
+
+    /* Claim more than dom1 already has fails with ENOMEM (claimed by dom2) */
+    ret = install_host_claims(dom1, domain_tot_pages(dom1) + 1);
+    CHECK(ret == -ENOMEM, "dom 1 claim +1 fails due to insufficient pages");
+
+    /* Claim more than dom1's d->max_pages fails with EINVAL */
+    ret = install_host_claims(dom1, dom1->max_pages + 1);
+    CHECK(ret == -EINVAL, "dom 1 claim fails due to exceeding max_pages");
+
+    /* Attempt to allocate an order-0 page with a foreign claim present */
+    pg = alloc_domheap_pages(dom1, order0, 0);
+    CHECK(pg == NULL, "dom 1 allocation fails because of domain 2's claim");
+    CHECK(TOTAL_CLAIMS == 1, "Expect domain 2's claim to be still present");
+    CHECK(FREE_PAGES == 1, "Expect one free page after failed alloc");
+}
+
+/*
+ * Test that memory claims can be cancelled by setting the claim count to 0,
+ * and that cancelled claims are freed up for other domains to claim.
+ *
+ * This is important for domain_kill() to be able to cancel claims of a dying
+ * domain and free up the pages for other domains to claim and free, otherwise
+ * the host might run out of free pages due to claims that are not released.
+ *
+ * - Test that after claiming pages for a domain, allocations redeem a portion
+ *   of those claims.
+ *
+ * - Test that other domains cannot claim more pages than the unclaimed free
+ *   pages, and that cancelled claims are no longer present after cancellation.
+ *
+ * - Test that after cancelling claims for a domain, other domains can claim
+ *   and allocate all remaining free pages.
+ */
+static void test_claim_alloc_cancel(int start_mfn)
+{
+    struct page_info *expected, *page = frame_table + start_mfn;
+    unsigned long heap_pages, claims;
+    unsigned int alloc_order;
+
+    /* Create a buddy of order 2 (4 pages) and add it to the heap. */
+    test_page_list_add_buddy(page, order3);
+    heap_pages = FREE_PAGES;
+    claims = heap_pages / 2;
+
+    /* Claim half of the free pages for domain 1 */
+    ASSERT(install_host_claims(dom1, claims) == 0);
+    ASSERT(TOTAL_CLAIMS == claims);
+
+    /* Allocate an order 1 page for domain 1 */
+    alloc_order = order1;
+    /* Expect the highest available page to be allocated */
+    expected = page + FREE_PAGES - (1UL << alloc_order);
+    ASSERT(alloc_domheap_pages(dom1, alloc_order, 0) == expected);
+    ASSERT(TOTAL_CLAIMS == (claims -= 1UL << alloc_order, claims));
+
+    /* Allocate an order 0 page for domain 1 */
+    alloc_order = order0;
+    /* Expect the highest available page to be allocated */
+    expected = page + FREE_PAGES - (1UL << alloc_order);
+    ASSERT(alloc_domheap_pages(dom1, alloc_order, 0) == expected);
+    ASSERT(TOTAL_CLAIMS == (claims -= 1UL << alloc_order, claims));
+
+    /* Claiming more than unclaimed for domain 2 should fail */
+    ASSERT(install_host_claims(dom2, heap_pages - claims + 1) == -ENOMEM);
+    /* Claiming all free pages for domain 2 should fail (dom1 has a claim) */
+    ASSERT(install_host_claims(dom2, FREE_PAGES) == -ENOMEM);
+    ASSERT(TOTAL_CLAIMS == claims);
+
+    /*
+     * Cancelling claims needs to always work, the checks in place for
+     * installing claims should not prevent cancelling claims, which is
+     * important for domain_kill() to be able to cancel claims of a dying
+     * domain regardless of the state of the domain's configuration.
+     *
+     * An important check that cancelling claims needs to bypass is the
+     * max_pages check, as a domain's max_pages can be set to a low value
+     * due to a toolstack process (Xapi's "squeezed" squeezing the domain
+     * can set its max_pages to a lower value than domain_tot_pages() by
+     * invoking do_domctl(XEN_DOMCTL_max_mem).
+     *
+     * This should not prevent the claims from being cancelled as required.
+     */
+    dom1->max_pages = domain_tot_pages(dom1) - 1;
+
+    /* Cancel all remaining claims for domain 1 */
+    ASSERT(install_host_claims(dom1, 0) == 0);
+    ASSERT(TOTAL_CLAIMS == 0);
+
+    /* Claim all free pages for domain 2, should work */
+    claims = FREE_PAGES;
+    ASSERT(install_host_claims(dom2, claims) == 0);
+    ASSERT(TOTAL_CLAIMS == claims);
+
+    /* Claiming for domain 1 should fail with EINVAL due to max_pages = 0 */
+    ASSERT(install_host_claims(dom1, 1) == -EINVAL);
+
+    /* With d->max_pages > domain_tot_pages(), dom1 claims fails with -ENOMEM 
*/
+    dom1->max_pages = heap_pages;
+    ASSERT(install_host_claims(dom1, 1) == -ENOMEM);
+
+    /* Attempting to allocate a page for domain 1 should likewise fail now */
+    ASSERT(alloc_domheap_pages(dom1, order0, 0) == NULL);
+
+    /* Allocating a page for domain 2 still work as it has the claims */
+    alloc_order = order0;
+    /* Expect the highest available page to be allocated */
+    expected = page + FREE_PAGES - (1UL << alloc_order);
+    ASSERT(alloc_domheap_pages(dom2, alloc_order, 0) == expected);
+    ASSERT(TOTAL_CLAIMS == (claims -= 1UL << alloc_order, claims));
+
+    /* Even allocating the remaining order 2 buddy for domain 2 works */
+    alloc_order = order2;
+    /* Expect the highest available page to be allocated */
+    expected = page + FREE_PAGES - (1UL << alloc_order);
+    ASSERT(alloc_domheap_pages(dom2, alloc_order, 0) == expected);
+    ASSERT(TOTAL_CLAIMS == (claims -= 1UL << alloc_order, claims));
+}
+
+/* Test offlining free pages outside and inside the claimed pages pool */
+static void test_offlining_host_claims(int start_mfn)
+{
+    struct page_info *pages = frame_table + start_mfn;
+    unsigned long heap_size, claims;
+
+    test_page_list_add_buddy(pages, order2);
+
+    heap_size = FREE_PAGES;
+    claims = heap_size - 1; /* Claim all but one page */
+    ASSERT(!install_host_claims(dom1, claims));
+
+    /* Mark a first page as offline */
+
+    mark_page_offline(pages + 3, 0);
+    ASSERT(page_state_is(pages + 3, offlined));
+
+    /* Due to the single unclaimed page, the claims should remain unchanged */
+    ASSERT(FREE_PAGES == heap_size);
+    ASSERT(TOTAL_CLAIMS == heap_size - 1);
+    ASSERT(reserve_offlined_page(pages) == 1);
+    ASSERT(FREE_PAGES == heap_size - 1); /* One free page is offlined */
+    ASSERT(TOTAL_CLAIMS == heap_size - 1);
+
+    /* Offline a second page. Offlines a portion of the claimed pages pool. */
+
+    mark_page_offline(pages + 1, 0);
+    ASSERT(page_state_is(pages + 1, offlined));
+
+    /* Assert the effect of offlining a portion of the claimed pages pool */
+    ASSERT(FREE_PAGES == heap_size - 1);
+    ASSERT(TOTAL_CLAIMS == heap_size - 1);
+    ASSERT(reserve_offlined_page(pages) == 1);
+    ASSERT(FREE_PAGES == heap_size - 2); /* Two pages are offlined */
+    ASSERT(TOTAL_CLAIMS == heap_size - 2); /* One claim is be released */
+}
+
+int main(int argc, char *argv[])
+{
+    const char *topic = "Test host-wide claims with old and new interfaces";
+
+    if ( !parse_args(argc, argv, topic) )
+        return EXIT_FAILURE;
+
+    init_page_alloc_tests();
+
+    /*
+     * Run the tests on all levels of the claims interface:
+     *
+     * 1. Direct page_alloc function call used by other code in the hypervisor,
+     *    which needs to support claim cancellation, needed by domain_kill().
+     *
+     * 2. The domctl helper for the hypercall, which is used by the hypercall
+     *    handler itself to parse the hypercall arguments before calling the
+     *    page_allocator functions.
+     *
+     * 3. The real DOMCTL handler, do_domctl(), which is the actual handler
+     *    function called when invoking the real hypercall.
+     */
+
+    /* Test using the direct claim function call used inside the hypervisor */
+    install_host_claims = install_host_claims_legacy;
+    RUN_TESTCASE("TCCD", test_claim_alloc_cancel, 8);
+    RUN_TESTCASE("DCGD", test_alloc_domheap_redeems_claims, 4);
+
+    /* Test claims setup using the actual DOMCTL handler itself, do_domctl() */
+    install_host_claims = xc_domain_claim_memory_host;
+    RUN_TESTCASE("TCCH", test_claim_alloc_cancel, 8);
+    RUN_TESTCASE("DCGH", test_alloc_domheap_redeems_claims, 4);
+
+    /* Test offlining free pages outside and inside the claimed pages pool */
+    RUN_TESTCASE("OHCH", test_offlining_host_claims, 4);
+
+    return test_complete();
+}
diff --git a/tools/tests/native/node-claims.c b/tools/tests/native/node-claims.c
new file mode 100644
index 000000000000..aa736f325f6b
--- /dev/null
+++ b/tools/tests/native/node-claims.c
@@ -0,0 +1,230 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Integration tests for NUMA-aware memory claims.
+ *
+ * The install test verifies that when a domain has a claim set installed
+ * with host-wide and per-NUMA-node claims, allocations that specify NUMA
+ * node affinity will redeem the appropriate claims (same-node first,
+ * host-wide fallback claim next, then other nodes, to not exceed page
+ * limits). It also verifies that the aggregate claim counters are updated
+ * correctly after each allocation.
+ *
+ * The get test verifies that callers can query the required number of
+ * claim records by passing a count of 0 and a NULL claim set buffer.
+ *
+ * Copyright (C) 2026 Cloud Software Group
+ */
+
+#ifdef __x86_64__
+#define CONFIG_NUMA 1 /* Enable NUMA support in the test environment. */
+#define TEST_ENABLE_XC_DOMAIN_C /* Enable xc_domain.c wrapper */
+#include "harness/native.h"
+
+typedef int (*set_numa_claims)
+    (struct domain *d, uint32_t entries, const xen_memory_claim_t *claim_set);
+set_numa_claims install_numa_claims;
+
+/*
+ * Test redeeming NUMA memory claims in exchange for allocations,
+ * where the redeemed claims are correctly reflected in the domain's
+ * claim state and the aggregate claim counters.
+ */
+static void test_claims_numa_install(int start_mfn)
+{
+    test_page_list_add_node_buddy(node0, start_mfn, order2);
+    test_page_list_add_node_buddy(node1, start_mfn, order2);
+
+    /* Install a claim set with host-wide + per-NUMA-node claims. */
+    xen_memory_claim_t claim_set[3] = {
+        { .target = XEN_DOMCTL_CLAIM_MEMORY_HOST, .pages = 2 },
+        { .target = node0, .pages = 2 },
+        { .target = node1, .pages = 2 },
+    };
+    int install = install_numa_claims(dom1, ARRAY_SIZE(claim_set), claim_set);
+
+    ASSERT(install == 0);
+    CHECK(TOTAL_CLAIMS == 6, "Expect 6 total claims after installation");
+    CLAIMS(dom1, claim_set);
+
+    ASSERT(alloc_domheap_pages(dom1, order0, MEMF_node(node0)));
+    CHECK(TOTAL_CLAIMS == 5, "Expect 5 total claims left after allocation");
+    CHECK(FREE_PAGES == 7, "Expect 7 free pages left after allocation");
+    ASSERT(claim_set[1].target == node0);
+    claim_set[1].pages--; /* Expect the allocation redeemed from node 0 */
+    CLAIMS(dom1, claim_set);
+
+    /* An allocation on node 1 redeems a claim from node 1 */
+    ASSERT(alloc_domheap_pages(dom1, order0, MEMF_node(node1)));
+    CHECK(TOTAL_CLAIMS == 4, "Expect 4 total claims left after allocation");
+    CHECK(FREE_PAGES == 6, "Expect 6 free pages left after allocation");
+    ASSERT(claim_set[2].target == node1);
+    claim_set[2].pages--; /* Expect the allocation redeemed from node 1 */
+    CLAIMS(dom1, claim_set);
+
+    /* An allocation on node 1 redeems the last claim from node 1 */
+    ASSERT(alloc_domheap_pages(dom1, order1, MEMF_node(node1)));
+    CHECK(TOTAL_CLAIMS == 2, "Expect 2 total claims left after allocation");
+    CHECK(FREE_PAGES == 4, "Expect 4 free pages left after allocation");
+    xen_memory_claim_t claim_set2[2] = {
+        claim_set[0], /* The Host-wide claim should still be present. */
+        claim_set[1], /* Claim from node 0 should still be present. */
+        /* The claim from node 1 is consumed, not part of the claim set. */
+    };
+    claim_set2[0].pages--; /* The 2nd page is redeemed from host-wide claim */
+    CLAIMS(dom1, claim_set2);
+
+    /* An allocation on node 1 falls back to the host-wide claim */
+    ASSERT(alloc_domheap_pages(dom1, order0, MEMF_node(node1)));
+    CHECK(TOTAL_CLAIMS == 1, "Expect 1 total claims left after allocation");
+    CHECK(FREE_PAGES == 3, "Expect 3 free pages left after allocation");
+    claim_set2[0].pages--; /* The 2nd page is redeemed from host-wide claim */
+    CLAIMS(dom1, claim_set2);
+
+    /* An allocation on node 1 falls back to node 0 */
+    ASSERT(alloc_domheap_pages(dom1, order0, MEMF_node(node1)));
+    CHECK(TOTAL_CLAIMS == 0, "Expect 0 total claims left after allocation");
+    CHECK(FREE_PAGES == 2, "Expect 2 free pages left after allocation");
+    CLAIMS(dom1, /* All claims should be consumed */
+           ((xen_memory_claim_t[]){
+                { .target = XEN_DOMCTL_CLAIM_MEMORY_HOST, .pages = 0 },
+            }));
+}
+
+/* Test getting the current claim set for a domain. */
+static void test_claims_numa_get(int start_mfn)
+{
+    test_page_list_add_node_buddy(node0, start_mfn, order2);
+    test_page_list_add_node_buddy(node1, start_mfn, order2);
+
+    /* Install a claim set with host-wide + per-NUMA-node claims. */
+    const xen_memory_claim_t claim_set[3] = {
+        { .target = XEN_DOMCTL_CLAIM_MEMORY_HOST, .pages = 2 },
+        { .target = node0, .pages = 2 },
+        { .target = node1, .pages = 2 },
+    };
+    int install = install_numa_claims(dom1, ARRAY_SIZE(claim_set), claim_set);
+
+    ASSERT(install == 0);
+
+    /*
+     * Assert that the direct call can get the number of claim records by
+     * passing a count of 0 and NULL for the claim set buffer.
+     */
+    uint32_t records = 0, expected_records = ARRAY_SIZE(claim_set);
+
+    ASSERT(domain_get_claim_entries(dom1, &records, NULL) == -ERANGE);
+    ASSERT(records == expected_records);
+
+    /*
+     * Assert that the libxc wrapper can get the number of claim records for
+     * a domain by passing a count of 0 and NULL for the claim set buffer.
+     */
+    records = 0;
+    ASSERT(xc_domain_claim_memory(&test_xc_handle, dom1->domain_id,
+                                  XEN_DOMCTL_CLAIM_MEMORY_GET,
+                                  &records, NULL) == -ERANGE);
+    ASSERT(records == expected_records);
+
+    /* Assert the libxc wrapper returning the expected claim set contents */
+    CLAIMS(dom1,
+           ((xen_memory_claim_t[]){
+                { .target = XEN_DOMCTL_CLAIM_MEMORY_HOST, .pages = 2 },
+                { .target = node0, .pages = 2 },
+                { .target = node1, .pages = 2 }
+            }));
+}
+
+/* Test offlining free pages outside and inside the claimed pages pool */
+static void test_offlining_node_claims(int start_mfn)
+{
+    struct page_info *pages = test_get_node_page(node0, start_mfn);
+    unsigned long heap_size, claims, host;
+
+    test_page_list_add_node_buddy(node0, start_mfn, order2);
+    test_page_list_add_node_buddy(node1, start_mfn, order2);
+    heap_size = FREE_PAGES;
+    claims = heap_size / 2 - 1; /* Claim all but 1 page on each node*/
+    host = heap_size - 2 * claims; /* Claim the rest host-wide */
+
+    /* Install a claim set with host-wide + per-NUMA-node claims. */
+    xen_memory_claim_t claim_set[] = {
+        { .target = XEN_DOMCTL_CLAIM_MEMORY_HOST, .pages = host },
+        { .target = node0, .pages = claims },
+        { .target = node1, .pages = claims },
+    };
+    ASSERT(install_numa_claims(dom1, ARRAY_SIZE(claim_set), claim_set) == 0);
+
+    /* Mark a first page as offline */
+
+    mark_page_offline(pages + 3, 0);
+    ASSERT(page_state_is(pages + 3, offlined));
+
+    /* The 1st page was not in a node's claims pool, but in the host pool */
+    ASSERT(FREE_PAGES == heap_size);
+    ASSERT(TOTAL_CLAIMS == heap_size);
+    ASSERT(reserve_offlined_page(pages) == 1);
+    ASSERT(FREE_PAGES == heap_size - 1); /* One free page is offlined */
+    ASSERT(TOTAL_CLAIMS == heap_size - 1);
+
+    /* Expect the pool of host-wide claims to be reduced by 1 page */
+    xen_memory_claim_t claim_set1[] = {
+        { .target = XEN_DOMCTL_CLAIM_MEMORY_HOST, .pages = host - 1 },
+        { .target = node0, .pages = claims },
+        { .target = node1, .pages = claims }
+    };
+    CLAIMS(dom1, claim_set1);
+
+    /* Offline a second page. Offlines a portion of the claimed pages pool. */
+
+    mark_page_offline(pages + 1, 0);
+    ASSERT(page_state_is(pages + 1, offlined));
+
+    /* Assert the effect of offlining a portion of the claimed pages pool */
+    ASSERT(FREE_PAGES == heap_size - 1);
+    ASSERT(TOTAL_CLAIMS == heap_size - 1);
+    ASSERT(reserve_offlined_page(pages) == 1);
+    ASSERT(FREE_PAGES == heap_size - 2); /* Two pages are offlined */
+    ASSERT(TOTAL_CLAIMS == heap_size - 2); /* One claim is be released */
+
+    /* The 2nd page was in the claims pool on node0, it should be released */
+    xen_memory_claim_t claim_set2[] = {
+                { .target = XEN_DOMCTL_CLAIM_MEMORY_HOST, .pages = host - 1 },
+                { .target = node0, .pages = claims - 1 },
+                { .target = node1, .pages = claims }
+            };
+    CLAIMS(dom1, claim_set2);
+}
+
+int main(int argc, char *argv[])
+{
+    const char *topic = "Test NUMA-aware claims with allocation from the heap";
+
+    if ( !parse_args(argc, argv, topic) )
+        return EXIT_FAILURE;
+
+    init_page_alloc_tests();
+
+    /* Run test cases with different NUMA claim installation methods */
+
+    /* Run the test with a direct call to domain_set_claim_entries() */
+    install_numa_claims = domain_set_claim_entries;
+    RUN_TESTCASE("CNIS", test_claims_numa_install, 4);
+
+    /* Run the test for getting the current claim set for a domain */
+    install_numa_claims = domain_set_claim_entries;
+    RUN_TESTCASE("CNGS", test_claims_numa_get, 4);
+
+    RUN_TESTCASE("ONCS", test_offlining_node_claims, 4);
+
+    return test_complete();
+}
+#else
+#include <stdio.h>
+int main(int argc, char *argv[])
+{
+    (void)argc;
+    (void)argv;
+    printf("This test requires NUMA, which is only available on x86_64.\n");
+    return 0;
+}
+#endif
-- 
2.39.5




 


Rackspace

Lists.xenproject.org is hosted with RackSpace, monitoring our
servers 24x7x365 and backed by RackSpace's Fanatical Support®.