diff --git a/tools/tests/Makefile b/tools/tests/Makefile index 6477a4386d..fc0ed80915 100644 --- a/tools/tests/Makefile +++ b/tools/tests/Makefile @@ -4,6 +4,7 @@ include $(XEN_ROOT)/tools/Rules.mk SUBDIRS-y := SUBDIRS-y += domid SUBDIRS-y += mem-claim +SUBDIRS-y += numa SUBDIRS-y += paging-mempool SUBDIRS-y += pdx SUBDIRS-y += rangeset diff --git a/tools/tests/numa/.gitignore b/tools/tests/numa/.gitignore new file mode 100644 index 0000000000..0710a767f4 --- /dev/null +++ b/tools/tests/numa/.gitignore @@ -0,0 +1,2 @@ +/numa.h +/test-numa diff --git a/tools/tests/numa/Makefile b/tools/tests/numa/Makefile new file mode 100644 index 0000000000..43e2db489d --- /dev/null +++ b/tools/tests/numa/Makefile @@ -0,0 +1,46 @@ +XEN_ROOT=$(CURDIR)/../../.. +include $(XEN_ROOT)/tools/Rules.mk + +TARGETS := test-numa + +.PHONY: all +all: $(TARGETS) + +.PHONY: run +run: $(TARGETS) +ifeq ($(CC),$(HOSTCC)) + set -e; \ + for test in $? ; do \ + ./$$test ; \ + done +else + $(warning HOSTCC != CC, will not run test) +endif + +.PHONY: clean +clean: + $(RM) -- *.o $(TARGETS) $(DEPS_RM) numa.h + +.PHONY: distclean +distclean: clean + $(RM) -- *~ + +.PHONY: install +install: all + $(INSTALL_DIR) $(DESTDIR)$(LIBEXEC)/tests + $(INSTALL_PROG) $(TARGETS) $(DESTDIR)$(LIBEXEC)/tests + +.PHONY: uninstall +uninstall: + $(RM) -- $(patsubst %,$(DESTDIR)$(LIBEXEC)/tests/%,$(TARGETS)) + +numa.h: $(XEN_ROOT)/xen/include/xen/numa.h + sed -e '/^#[[:space:]]*include/d' <$< >$@ + +CFLAGS += -D__XEN_TOOLS__ +CFLAGS += $(CFLAGS_xeninclude) + +test-numa: test-numa.c numa.h + $(CC) $(CPPFLAGS) $(CFLAGS) -o $@ $< $(APPEND_CFLAGS) + +-include $(DEPS_INCLUDE) diff --git a/tools/tests/numa/test-numa.c b/tools/tests/numa/test-numa.c new file mode 100644 index 0000000000..ed91a2824f --- /dev/null +++ b/tools/tests/numa/test-numa.c @@ -0,0 +1,218 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Unit tests for NUMA setup. + * + * Copyright (C) 2026 Cloud Software Group + */ + +#include "wrapped-xen-numa.h" +#include "../../xen/common/numa.c" + +static void numa_reset_state(void) +{ + bitmap_clear(processor_nodes_parsed.bits, CONFIG_NR_NUMA_NODES); + bitmap_clear(memory_nodes_parsed.bits, CONFIG_NR_NUMA_NODES); + bitmap_clear(memblk_hotplug, NR_NODE_MEMBLKS); + memset(numa_nodes, 0, sizeof(numa_nodes)); + memset(node_memblk_range, 0, sizeof(node_memblk_range)); + memset(memblk_nodeid, 0, sizeof(memblk_nodeid)); + memset(node_data, 0, sizeof(node_data)); + memset(node_to_cpumask, 0, sizeof(node_to_cpumask)); + memset(cpu_to_node, NUMA_NO_NODE, sizeof(cpu_to_node)); + num_node_memblks = 0; + memnode_shift = 0; + memnodemapsize = 0; + if ( memnodemap != _memnodemap ) + free(memnodemap); + memnodemap = NULL; + bitmap_clear(node_online_map.bits, CONFIG_NR_NUMA_NODES); + node_set(1, node_online_map); +} + +struct mem_affinity { + /* Ranges are defined as [start, end]. */ + paddr_t start, end; + unsigned int nid; +}; + +struct mem_range { + /* Ranges are defined as [start, end]. */ + paddr_t start, end; +}; + +const static struct mem_range *ram; + +int arch_get_ram_range(unsigned int idx, paddr_t *start, paddr_t *end) +{ + if ( idx >= MAX_RANGES || !ram[idx].end ) + return -ENOENT; + + *start = ram[idx].start; + *end = ram[idx].end + 1; + + return 0; +} + +static void print_ranges(const struct mem_affinity *r) +{ + unsigned int i; + + printf("Affinity ranges:\n"); + for ( i = 0; i < MAX_RANGES; i++ ) + { + if ( !r[i].end ) + break; + + printf(" NID %u [%" PRIpaddr ", %" PRIpaddr "]\n", + r[i].nid, r[i].start, r[i].end); + } + + printf("RAM ranges:\n"); + for ( i = 0; i < MAX_RANGES; i++ ) + { + if ( !ram[i].end ) + break; + + printf(" [%" PRIpaddr ", %" PRIpaddr "]\n", + ram[i].start, ram[i].end); + } +} + +static bool test_paddr(paddr_t addr) +{ + mfn_t mfn = PFN_DOWN(addr); + unsigned int idx = mfn >> memnode_shift; + unsigned int nid; + + if ( idx >= memnodemapsize ) + { + printf("Fail: MFN %lx -> IDX %u outside of memnodemap range\n", + mfn, idx); + return false; + } + + nid = memnodemap[idx]; + if ( nid >= MAX_NUMNODES ) + { + printf("Fail: MFN %lx -> NID %u >= MAX_NUMNODES (%u)\n", + mfn, nid, MAX_NUMNODES); + return false; + } + + if ( !node_data[nid].node_spanned_pages ) + { + printf("Fail: MFN %lx -> NID %u without spanned pages\n", + mfn, nid); + return false; + + } + + if ( !node_data[nid].node_spanned_pages ) + { + printf("Fail: MFN %lx -> NID %u without spanned pages\n", + mfn, nid); + return false; + + } + + if ( !node_data[nid].node_spanned_pages ) + { + printf("Fail: MFN %lx outside NID range [%013lx, %013lx]\n", + mfn, node_data[nid].node_start_pfn, + node_data[nid].node_start_pfn + + node_data[nid].node_spanned_pages - 1); + return false; + } + + return true; +} + +int main(int argc, char **argv) +{ + static const struct { + struct mem_affinity affinity[MAX_RANGES]; + struct mem_range ram[MAX_RANGES]; + } tests[] = { + /* From an arbitrary AMD Turin system. */ + { + .affinity = { + { .nid = 0, .start = 0x00000000000ULL, .end = 0x0000009ffffULL }, + { .nid = 0, .start = 0x000000c0000ULL, .end = 0x000afffffffULL }, + { .nid = 0, .start = 0x00100000000ULL, .end = 0x0c04fffffffULL }, + { .nid = 1, .start = 0x0c050000000ULL, .end = 0x0fc4fffffffULL }, + { .nid = 1, .start = 0x10000000000ULL, .end = 0x183ffffffffULL }, + }, + .ram = { + { .start = 0x00000000000ULL, .end = 0x0000009ffffULL }, + { .start = 0x00000100000ULL, .end = 0x0007590ffffULL }, + { .start = 0x000759d1000ULL, .end = 0x00075a0ffffULL }, + { .start = 0x00076000000ULL, .end = 0x00094c73fffULL }, + { .start = 0x0009b5ff000ULL, .end = 0x0009fff9fffULL }, + { .start = 0x0009ffff000ULL, .end = 0x0009fffffffULL }, + { .start = 0x00100010000ULL, .end = 0x0fc4fffffffULL }, + { .start = 0x10000000000ULL, .end = 0x183f7ffffffULL }, + { .start = 0x183f8800000ULL, .end = 0x183faabffffULL }, + }, + }, + }; + int ret_code = EXIT_SUCCESS; + + /* Dummy firmware interface provider name, use TST for TEST. */ + numa_fw_nid_name = "TST"; + + for ( unsigned int i = 0 ; i < ARRAY_SIZE(tests); i++ ) + { + paddr_t min = ~(paddr_t)0, max = 0; + unsigned int j; + + numa_reset_state(); + + ram = tests[i].ram; + + for ( j = 0; + j < ARRAY_SIZE(tests[i].affinity) && tests[i].affinity[j].end; + j++ ) + { + const struct mem_affinity *affinity = &tests[i].affinity[j]; + paddr_t length = affinity->end - affinity->start + 1; + + if ( !numa_update_node_memblks(affinity->nid, affinity->nid, + affinity->start, length, false) ) + { + printf("Fail to add NID %u [%" PRIpaddr ", %" PRIpaddr "]\n", + affinity->nid, affinity->start, affinity->end); + ret_code = EXIT_FAILURE; + continue; + } + + min = min(min, affinity->start); + max = max(max, affinity->end); + } + + if ( !numa_process_nodes(min, max + 1) ) + { + printf("Unable to process nodes\n"); + print_ranges(tests[i].affinity); + ret_code = EXIT_FAILURE; + continue; + } + + for ( j = 0; + j < ARRAY_SIZE(tests[i].ram) && tests[i].ram[j].end; + j++ ) + if ( !test_paddr(tests[i].ram[j].start) || + !test_paddr(tests[i].ram[j].end) ) + ret_code = EXIT_FAILURE; + } + + return ret_code; +} + +/* + * Local variables: + * mode: C + * c-file-style: "BSD" + * c-basic-offset: 4 + * indent-tabs-mode: nil + * End: + */ diff --git a/tools/tests/numa/wrapped-xen-numa.h b/tools/tests/numa/wrapped-xen-numa.h new file mode 100644 index 0000000000..ed97524e48 --- /dev/null +++ b/tools/tests/numa/wrapped-xen-numa.h @@ -0,0 +1,187 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Unit tests for NUMA setup. + * + * Copyright (C) 2026 Cloud Software Group + */ + +#ifndef WRAPPED_XEN_NUMA_H +#define WRAPPED_XEN_NUMA_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#define CONFIG_DEBUG +#define CONFIG_NUMA +#define CONFIG_NR_NUMA_NODES 64 +#define NR_CPUS 256 +#define MAX_RANGES 128 +#define PADDR_BITS 52 + +#define __init +#define __initdata +#define __ro_after_init +#define __read_mostly + +#define printk printf +#define XENLOG_INFO "" +#define XENLOG_DEBUG "" +#define XENLOG_WARNING "" +#define KERN_INFO "" +#define KERN_ERR "" +#define KERN_WARNING "" +#define KERN_DEBUG "" + +#define PAGE_SHIFT 12 +/* Some libcs define PAGE_SIZE in limits.h. */ +#undef PAGE_SIZE +#define PAGE_SIZE (1L << PAGE_SHIFT) +#define MAX_ORDER 18 /* 2 * PAGETABLE_ORDER (9) */ + +#define PFN_DOWN(x) ((x) >> PAGE_SHIFT) +#define PFN_UP(x) (((x) + PAGE_SIZE-1) >> PAGE_SHIFT) + +#define paddr_to_pfn(pa) ((unsigned long)((pa) >> PAGE_SHIFT)) +#define mfn_to_pdx(mfn) (mfn) +#define paddr_to_pdx(pa) ((pa) >> PAGE_SHIFT) +#define mfn_to_maddr(mfn) ((mfn) << PAGE_SHIFT) + +#define ASSERT assert +#define ASSERT_UNREACHABLE() assert(0) + +/* For the purposes of the testing assume arch NID == Xen NID. */ +#define numa_node_to_arch_nid(n) (n) + +typedef uint64_t paddr_t; +#define PRIpaddr "016" PRIx64 + +typedef unsigned long mfn_t; +typedef uint8_t nodeid_t; + +#define __set_bit set_bit +#define __clear_bit clear_bit + +static inline unsigned int find_next_bit( + const unsigned long *addr, unsigned int size, unsigned int off) +{ + unsigned int i; + + ASSERT(size <= BITS_PER_LONG); + + for ( i = off; i < size; i++ ) + if ( *addr & (1UL << i) ) + return i; + + return size; +} + +#define find_first_bit(b, s) find_next_bit(b, s, 0) + +/* Minimal cpumask support. */ +typedef struct cpumask{ DECLARE_BITMAP(bits, NR_CPUS); } cpumask_t; + +#define cpumask_clear_cpu(c, m) clear_bit(c, (m)->bits) + +/* Define the nodemask helpers used. */ +typedef struct nodemask{ DECLARE_BITMAP(bits, CONFIG_NR_NUMA_NODES); } nodemask_t; + +#define node_set(node, dst) set_bit(node, (dst).bits) + +#define first_node(n) __first_node(&(n), CONFIG_NR_NUMA_NODES) +static inline int __first_node(const nodemask_t *srcp, unsigned int s) +{ + return min(s, find_next_bit(srcp->bits, s, 0)); +} + +#define next_node(n, m) __next_node(n, &(m), CONFIG_NR_NUMA_NODES) +static inline int __next_node(unsigned int n, const nodemask_t *srcp, + unsigned int s) +{ + return min(s, find_next_bit(srcp->bits, s, n + 1)); +} + +#define nodes_or(dst, src1, src2) \ + bitmap_or((dst).bits, (src1).bits, (src2).bits, CONFIG_NR_NUMA_NODES) + +static inline bool nodemask_test(unsigned int node, const nodemask_t *dst) +{ + return test_bit(node, dst->bits); +} + +#define node_set_online(node) set_bit(node, node_online_map.bits) + +#define cycle_node(n, src) __cycle_node(n, &(src), MAX_NUMNODES) +static inline int __cycle_node(int n, const nodemask_t *maskp, + unsigned int nbits) +{ + unsigned int nxt = __next_node(n, maskp, nbits); + + if ( nxt == nbits ) + nxt = __first_node(maskp, nbits); + + return nxt; +} + +#define for_each_node_mask(node, mask) \ + for ( (node) = first_node(mask); \ + (node) < MAX_NUMNODES; \ + (node) = next_node(node, mask) ) + +/* + * Dummy helper to satisfy allocate_cachealigned_memnodemap(), the memory + * allocation is instead done in vmap_contig(). + */ +static inline mfn_t alloc_boot_pages(unsigned long nr, unsigned long align) +{ + return 0; +} + +static inline void *vmap_contig(mfn_t mfn, unsigned int nr) +{ + assert(!mfn); + return calloc(PAGE_SIZE, nr); +} + +static inline void panic(const char *msg) +{ + printf("%s\n", msg); + abort(); +} + +/* Dummy implementations to satisfy the build. */ +static inline bool arch_numa_disabled(void) +{ + return false; +} + +static inline void numa_fw_bad(void) { } + +static inline bool arch_numa_unavailable(void) +{ + return false; +} + +static paddr_t mem_hotplug; +static unsigned int __read_mostly nr_cpu_ids = NR_CPUS; + +#include "numa.h" + +#endif + +/* + * Local variables: + * mode: C + * c-file-style: "BSD" + * c-basic-offset: 4 + * indent-tabs-mode: nil + * End: + */