811 lines
23 KiB
C
811 lines
23 KiB
C
/*
|
|
* test_vmem.c — Tests for vmem budget-tracked virtual memory allocator,
|
|
* arena-vmem integration, and slab+vmem parallel extraction.
|
|
*/
|
|
#include "test_framework.h"
|
|
#include "test_helpers.h"
|
|
#include "../src/foundation/vmem.h"
|
|
#include "../src/foundation/arena.h"
|
|
#include "../src/foundation/slab_alloc.h"
|
|
#include "pipeline/pipeline.h"
|
|
#include "pipeline/pipeline_internal.h"
|
|
#include "graph_buffer/graph_buffer.h"
|
|
#include "discover/discover.h"
|
|
#include "cbm.h"
|
|
|
|
#include <stdatomic.h>
|
|
#include <sys/stat.h>
|
|
|
|
/* ── vmem basic tests ─────────────────────────────────────────── */
|
|
|
|
TEST(vmem_budget_zero_before_init) {
|
|
/* Before init, budget should be 0 — arenas/read_file fall back to malloc */
|
|
/* NOTE: vmem_init uses atomic CAS, only first call takes effect.
|
|
* These tests verify the pre-init state if vmem was never initialized,
|
|
* or the post-init state if it was. We test what we can. */
|
|
size_t budget = cbm_vmem_budget();
|
|
/* Budget is either 0 (never inited) or >0 (inited by earlier test/main) */
|
|
(void)budget;
|
|
PASS();
|
|
}
|
|
|
|
TEST(vmem_alloc_and_free) {
|
|
/* Allocate 1MB, write to it, verify, free */
|
|
size_t sz = 1024 * 1024;
|
|
void *p = cbm_vmem_alloc(sz);
|
|
if (!p) {
|
|
/* vmem may not be initialized — skip gracefully */
|
|
PASS();
|
|
}
|
|
/* vmem guarantees zeroed memory */
|
|
unsigned char *bytes = (unsigned char *)p;
|
|
for (size_t i = 0; i < sz; i += 4096) {
|
|
ASSERT_EQ(bytes[i], 0);
|
|
}
|
|
/* Write pattern */
|
|
memset(p, 0xAB, sz);
|
|
ASSERT_EQ(bytes[0], 0xAB);
|
|
ASSERT_EQ(bytes[sz - 1], 0xAB);
|
|
|
|
cbm_vmem_free(p, sz);
|
|
PASS();
|
|
}
|
|
|
|
TEST(vmem_alloc_zero_returns_zeroed) {
|
|
size_t sz = 64 * 1024;
|
|
void *p = cbm_vmem_alloc(sz);
|
|
if (!p) {
|
|
PASS();
|
|
}
|
|
unsigned char *bytes = (unsigned char *)p;
|
|
int nonzero = 0;
|
|
for (size_t i = 0; i < sz; i++) {
|
|
if (bytes[i] != 0) {
|
|
nonzero++;
|
|
}
|
|
}
|
|
ASSERT_EQ(nonzero, 0);
|
|
cbm_vmem_free(p, sz);
|
|
PASS();
|
|
}
|
|
|
|
TEST(vmem_budget_tracking) {
|
|
size_t before = cbm_vmem_allocated();
|
|
size_t sz = 256 * 1024;
|
|
void *p = cbm_vmem_alloc(sz);
|
|
if (!p) {
|
|
PASS();
|
|
}
|
|
size_t after = cbm_vmem_allocated();
|
|
/* allocated should have increased (may be rounded to page) */
|
|
ASSERT_GT(after, before);
|
|
|
|
cbm_vmem_free(p, sz);
|
|
size_t freed = cbm_vmem_allocated();
|
|
/* Should be back to (approximately) before */
|
|
ASSERT_LTE(freed, before + 4096); /* within one page */
|
|
PASS();
|
|
}
|
|
|
|
TEST(vmem_peak_tracks) {
|
|
size_t sz = 512 * 1024;
|
|
void *p1 = cbm_vmem_alloc(sz);
|
|
if (!p1) {
|
|
PASS();
|
|
}
|
|
size_t peak1 = cbm_vmem_peak();
|
|
ASSERT_GT(peak1, 0);
|
|
|
|
void *p2 = cbm_vmem_alloc(sz);
|
|
if (!p2) {
|
|
cbm_vmem_free(p1, sz);
|
|
PASS();
|
|
}
|
|
size_t peak2 = cbm_vmem_peak();
|
|
ASSERT_GTE(peak2, peak1);
|
|
|
|
cbm_vmem_free(p2, sz);
|
|
cbm_vmem_free(p1, sz);
|
|
|
|
/* Peak should not decrease after free */
|
|
size_t peak3 = cbm_vmem_peak();
|
|
ASSERT_GTE(peak3, peak2);
|
|
PASS();
|
|
}
|
|
|
|
TEST(vmem_worker_budget) {
|
|
size_t budget = cbm_vmem_budget();
|
|
if (budget == 0) {
|
|
/* Not initialized — worker budget should be 0 */
|
|
ASSERT_EQ(cbm_vmem_worker_budget(4), 0);
|
|
PASS();
|
|
}
|
|
/* Budget divides correctly */
|
|
size_t wb4 = cbm_vmem_worker_budget(4);
|
|
size_t wb8 = cbm_vmem_worker_budget(8);
|
|
ASSERT_EQ(wb4, budget / 4);
|
|
ASSERT_EQ(wb8, budget / 8);
|
|
/* Edge case: 0 workers */
|
|
ASSERT_EQ(cbm_vmem_worker_budget(0), 0);
|
|
PASS();
|
|
}
|
|
|
|
/* ── vmem edge-case and resource management tests ────────────── */
|
|
|
|
TEST(vmem_alloc_zero_returns_null) {
|
|
cbm_vmem_init(0.5);
|
|
/* alloc(0) must return NULL per the API contract */
|
|
void *p = cbm_vmem_alloc(0);
|
|
ASSERT_NULL(p);
|
|
PASS();
|
|
}
|
|
|
|
TEST(vmem_free_null_zero_no_crash) {
|
|
cbm_vmem_init(0.5);
|
|
/* free(NULL, 0) must be a no-op */
|
|
cbm_vmem_free(NULL, 0);
|
|
PASS();
|
|
}
|
|
|
|
TEST(vmem_free_null_nonzero_no_crash) {
|
|
cbm_vmem_init(0.5);
|
|
/* free(NULL, 100) must be a no-op — ptr==NULL short-circuits */
|
|
cbm_vmem_free(NULL, 100);
|
|
PASS();
|
|
}
|
|
|
|
TEST(vmem_alloc_very_large) {
|
|
cbm_vmem_init(0.5);
|
|
/* Attempt 1 GB allocation — may succeed or fail depending on system.
|
|
* Either way it must not crash. */
|
|
size_t sz = (size_t)1024 * 1024 * 1024;
|
|
void *p = cbm_vmem_alloc(sz);
|
|
if (p) {
|
|
/* If it succeeded, verify we can touch the first and last page */
|
|
((unsigned char *)p)[0] = 0xAA;
|
|
((unsigned char *)p)[sz - 1] = 0xBB;
|
|
cbm_vmem_free(p, sz);
|
|
}
|
|
/* Success or graceful NULL — either is fine */
|
|
PASS();
|
|
}
|
|
|
|
TEST(vmem_sequential_alloc_free_no_leak) {
|
|
cbm_vmem_init(0.5);
|
|
size_t before = cbm_vmem_allocated();
|
|
|
|
/* 20 alloc/free cycles — allocated must return to baseline */
|
|
size_t sz = 64 * 1024; /* 64 KB */
|
|
for (int i = 0; i < 20; i++) {
|
|
void *p = cbm_vmem_alloc(sz);
|
|
ASSERT_NOT_NULL(p);
|
|
memset(p, (unsigned char)(i & 0xFF), sz);
|
|
cbm_vmem_free(p, sz);
|
|
}
|
|
|
|
size_t after = cbm_vmem_allocated();
|
|
ASSERT_EQ(after, before);
|
|
PASS();
|
|
}
|
|
|
|
TEST(vmem_worker_budget_negative_workers) {
|
|
cbm_vmem_init(0.5);
|
|
size_t budget = cbm_vmem_budget();
|
|
if (budget == 0) {
|
|
/* Not initialized in this process — skip */
|
|
PASS();
|
|
}
|
|
/* Negative workers clamps to 1 → worker_budget == full budget */
|
|
size_t wb = cbm_vmem_worker_budget(-3);
|
|
ASSERT_EQ(wb, budget);
|
|
PASS();
|
|
}
|
|
|
|
TEST(vmem_over_budget_when_nothing_allocated) {
|
|
cbm_vmem_init(0.5);
|
|
/* With nothing (or near-nothing) allocated, should not be over budget */
|
|
bool over = cbm_vmem_over_budget();
|
|
ASSERT_FALSE(over);
|
|
PASS();
|
|
}
|
|
|
|
TEST(vmem_allocated_tracks_alloc_free_cycle) {
|
|
cbm_vmem_init(0.5);
|
|
size_t base = cbm_vmem_allocated();
|
|
|
|
size_t sz = 128 * 1024; /* 128 KB */
|
|
void *p = cbm_vmem_alloc(sz);
|
|
ASSERT_NOT_NULL(p);
|
|
|
|
size_t after_alloc = cbm_vmem_allocated();
|
|
ASSERT_GT(after_alloc, base);
|
|
|
|
cbm_vmem_free(p, sz);
|
|
|
|
size_t after_free = cbm_vmem_allocated();
|
|
/* Must be back at or near baseline */
|
|
ASSERT_LTE(after_free, base + 4096);
|
|
PASS();
|
|
}
|
|
|
|
TEST(vmem_multiple_alloc_tracks_cumulative) {
|
|
cbm_vmem_init(0.5);
|
|
size_t base = cbm_vmem_allocated();
|
|
|
|
size_t sz = 64 * 1024; /* 64 KB */
|
|
void *p1 = cbm_vmem_alloc(sz);
|
|
ASSERT_NOT_NULL(p1);
|
|
size_t after_one = cbm_vmem_allocated();
|
|
|
|
void *p2 = cbm_vmem_alloc(sz);
|
|
ASSERT_NOT_NULL(p2);
|
|
size_t after_two = cbm_vmem_allocated();
|
|
|
|
/* Two allocs should be roughly double one alloc above base */
|
|
ASSERT_GT(after_two, after_one);
|
|
ASSERT_GT(after_one, base);
|
|
|
|
cbm_vmem_free(p2, sz);
|
|
cbm_vmem_free(p1, sz);
|
|
|
|
size_t after_free = cbm_vmem_allocated();
|
|
ASSERT_LTE(after_free, base + 4096);
|
|
PASS();
|
|
}
|
|
|
|
TEST(vmem_peak_never_decreases) {
|
|
cbm_vmem_init(0.5);
|
|
|
|
size_t sz = 256 * 1024;
|
|
void *p = cbm_vmem_alloc(sz);
|
|
ASSERT_NOT_NULL(p);
|
|
size_t peak_with_alloc = cbm_vmem_peak();
|
|
|
|
cbm_vmem_free(p, sz);
|
|
size_t peak_after_free = cbm_vmem_peak();
|
|
|
|
/* Peak must never decrease */
|
|
ASSERT_GTE(peak_after_free, peak_with_alloc);
|
|
PASS();
|
|
}
|
|
|
|
TEST(vmem_worker_budget_one_worker) {
|
|
cbm_vmem_init(0.5);
|
|
size_t budget = cbm_vmem_budget();
|
|
if (budget == 0) {
|
|
PASS();
|
|
}
|
|
/* 1 worker → equals full budget */
|
|
size_t wb = cbm_vmem_worker_budget(1);
|
|
ASSERT_EQ(wb, budget);
|
|
PASS();
|
|
}
|
|
|
|
TEST(vmem_worker_budget_many_workers) {
|
|
cbm_vmem_init(0.5);
|
|
size_t budget = cbm_vmem_budget();
|
|
if (budget == 0) {
|
|
PASS();
|
|
}
|
|
/* 1000 workers → must still be non-zero (budget is huge) */
|
|
size_t wb = cbm_vmem_worker_budget(1000);
|
|
ASSERT_GT(wb, 0);
|
|
ASSERT_EQ(wb, budget / 1000);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Arena-vmem integration tests ─────────────────────────────── */
|
|
|
|
TEST(arena_vmem_alloc_and_destroy) {
|
|
/* When vmem is initialized, arena should use vmem for blocks.
|
|
* When not initialized, falls back to malloc. Either way, this must work. */
|
|
CBMArena a;
|
|
cbm_arena_init(&a);
|
|
ASSERT_EQ(a.nblocks, 1);
|
|
/* block_sizes[0] should track the initial block size */
|
|
ASSERT_EQ(a.block_sizes[0], CBM_ARENA_DEFAULT_BLOCK_SIZE);
|
|
|
|
/* Allocate some data */
|
|
char *s = cbm_arena_strdup(&a, "hello vmem integration");
|
|
ASSERT_NOT_NULL(s);
|
|
ASSERT_STR_EQ(s, "hello vmem integration");
|
|
|
|
cbm_arena_destroy(&a);
|
|
ASSERT_EQ(a.nblocks, 0);
|
|
PASS();
|
|
}
|
|
|
|
TEST(arena_vmem_grow_tracks_sizes) {
|
|
CBMArena a;
|
|
cbm_arena_init_sized(&a, 64);
|
|
ASSERT_EQ(a.block_sizes[0], 64);
|
|
|
|
/* Force growth */
|
|
cbm_arena_alloc(&a, 48);
|
|
cbm_arena_alloc(&a, 48); /* triggers grow */
|
|
ASSERT_GTE(a.nblocks, 2);
|
|
/* Second block should be larger */
|
|
ASSERT_GT(a.block_sizes[1], 0);
|
|
ASSERT_GTE(a.block_sizes[1], 96); /* at least min_size */
|
|
|
|
cbm_arena_destroy(&a);
|
|
PASS();
|
|
}
|
|
|
|
TEST(arena_vmem_large_alloc) {
|
|
/* Allocate > 64KB to test vmem for larger arena blocks */
|
|
CBMArena a;
|
|
cbm_arena_init(&a);
|
|
|
|
size_t big = 128 * 1024;
|
|
void *p = cbm_arena_alloc(&a, big);
|
|
ASSERT_NOT_NULL(p);
|
|
|
|
/* Write pattern to verify memory is writable */
|
|
memset(p, 0xCD, big);
|
|
unsigned char *bytes = (unsigned char *)p;
|
|
ASSERT_EQ(bytes[0], 0xCD);
|
|
ASSERT_EQ(bytes[big - 1], 0xCD);
|
|
|
|
cbm_arena_destroy(&a);
|
|
PASS();
|
|
}
|
|
|
|
TEST(arena_vmem_reset_frees_blocks) {
|
|
CBMArena a;
|
|
cbm_arena_init_sized(&a, 128);
|
|
|
|
/* Create multiple blocks */
|
|
cbm_arena_alloc(&a, 100);
|
|
cbm_arena_alloc(&a, 100);
|
|
ASSERT_GTE(a.nblocks, 2);
|
|
|
|
/* Reset should free extra blocks */
|
|
cbm_arena_reset(&a);
|
|
ASSERT_EQ(a.nblocks, 1);
|
|
ASSERT_EQ(a.block_sizes[1], 0); /* freed block's size cleared */
|
|
|
|
/* Should still be usable */
|
|
void *p = cbm_arena_alloc(&a, 16);
|
|
ASSERT_NOT_NULL(p);
|
|
|
|
cbm_arena_destroy(&a);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Tier 2 slab allocator tests ──────────────────────────── */
|
|
|
|
TEST(tier2_alloc_and_free_128) {
|
|
/* Allocate 100 bytes — rounds up to 128-byte class (with 16-byte header) */
|
|
cbm_vmem_init(0.5);
|
|
cbm_slab_install();
|
|
|
|
void *p = cbm_slab_test_malloc(100);
|
|
ASSERT_NOT_NULL(p);
|
|
|
|
/* Verify memory is writable */
|
|
memset(p, 0xAA, 100);
|
|
ASSERT_EQ(((unsigned char *)p)[0], 0xAA);
|
|
ASSERT_EQ(((unsigned char *)p)[99], 0xAA);
|
|
|
|
cbm_slab_test_free(p);
|
|
|
|
/* Allocate again — should reuse from free list (same size class) */
|
|
void *p2 = cbm_slab_test_malloc(100);
|
|
ASSERT_NOT_NULL(p2);
|
|
/* May or may not be same address, but must be valid */
|
|
memset(p2, 0xBB, 100);
|
|
cbm_slab_test_free(p2);
|
|
|
|
cbm_slab_destroy_thread();
|
|
PASS();
|
|
}
|
|
|
|
TEST(tier2_alloc_all_classes) {
|
|
/* Test all 6 size classes: 128, 256, 512, 1024, 2048, 4096 */
|
|
cbm_vmem_init(0.5);
|
|
cbm_slab_install();
|
|
|
|
size_t test_sizes[] = {65, 200, 300, 800, 1500, 3000};
|
|
void *ptrs[6];
|
|
|
|
for (int i = 0; i < 6; i++) {
|
|
ptrs[i] = cbm_slab_test_malloc(test_sizes[i]);
|
|
ASSERT_NOT_NULL(ptrs[i]);
|
|
/* Write pattern to verify each allocation is independent */
|
|
memset(ptrs[i], (unsigned char)(0x10 + i), test_sizes[i]);
|
|
}
|
|
|
|
/* Verify patterns are intact (no overlap) */
|
|
for (int i = 0; i < 6; i++) {
|
|
unsigned char *bytes = (unsigned char *)ptrs[i];
|
|
ASSERT_EQ(bytes[0], (unsigned char)(0x10 + i));
|
|
ASSERT_EQ(bytes[test_sizes[i] - 1], (unsigned char)(0x10 + i));
|
|
}
|
|
|
|
/* Free all */
|
|
for (int i = 0; i < 6; i++) {
|
|
cbm_slab_test_free(ptrs[i]);
|
|
}
|
|
|
|
cbm_slab_destroy_thread();
|
|
PASS();
|
|
}
|
|
|
|
TEST(tier2_free_list_reuse) {
|
|
/* Verify free list provides O(1) reuse within same size class */
|
|
cbm_vmem_init(0.5);
|
|
cbm_slab_install();
|
|
|
|
/* Allocate and free 10 blocks of class 256 */
|
|
void *addrs[10];
|
|
for (int i = 0; i < 10; i++) {
|
|
addrs[i] = cbm_slab_test_malloc(200);
|
|
ASSERT_NOT_NULL(addrs[i]);
|
|
}
|
|
for (int i = 0; i < 10; i++) {
|
|
cbm_slab_test_free(addrs[i]);
|
|
}
|
|
|
|
/* Re-allocate 10 blocks — all should come from free list
|
|
* (LIFO order means addrs come back in reverse) */
|
|
for (int i = 0; i < 10; i++) {
|
|
void *p = cbm_slab_test_malloc(200);
|
|
ASSERT_NOT_NULL(p);
|
|
memset(p, 0xCC, 200);
|
|
cbm_slab_test_free(p);
|
|
}
|
|
|
|
cbm_slab_destroy_thread();
|
|
PASS();
|
|
}
|
|
|
|
TEST(tier2_oversized_dedicated) {
|
|
/* Allocate >4096 bytes — gets dedicated page, freed immediately on free() */
|
|
cbm_vmem_init(0.5);
|
|
cbm_slab_install();
|
|
|
|
size_t before_alloc = cbm_vmem_allocated();
|
|
|
|
void *big = cbm_slab_test_malloc(8192);
|
|
ASSERT_NOT_NULL(big);
|
|
memset(big, 0xDD, 8192);
|
|
|
|
size_t after_alloc = cbm_vmem_allocated();
|
|
/* vmem allocated should have grown for the dedicated page */
|
|
ASSERT_GT(after_alloc, before_alloc);
|
|
|
|
/* Free — dedicated page should be vmem_free'd immediately */
|
|
cbm_slab_test_free(big);
|
|
|
|
size_t after_free = cbm_vmem_allocated();
|
|
/* vmem allocated should have decreased (page was freed) */
|
|
ASSERT_LTE(after_free, after_alloc);
|
|
|
|
cbm_slab_destroy_thread();
|
|
PASS();
|
|
}
|
|
|
|
TEST(tier2_realloc_same_class) {
|
|
/* realloc within same size class should return same pointer */
|
|
cbm_vmem_init(0.5);
|
|
cbm_slab_install();
|
|
|
|
void *p = cbm_slab_test_malloc(100);
|
|
ASSERT_NOT_NULL(p);
|
|
memset(p, 0xEE, 100);
|
|
|
|
/* Grow to 110 — still fits in 128-byte class */
|
|
void *p2 = cbm_slab_test_realloc(p, 110);
|
|
ASSERT_NOT_NULL(p2);
|
|
ASSERT_EQ(p, p2); /* same pointer, same class */
|
|
/* Original data should be preserved */
|
|
ASSERT_EQ(((unsigned char *)p2)[0], 0xEE);
|
|
ASSERT_EQ(((unsigned char *)p2)[99], 0xEE);
|
|
|
|
/* Shrink to 70 — still fits in 128-byte class */
|
|
void *p3 = cbm_slab_test_realloc(p2, 70);
|
|
ASSERT_NOT_NULL(p3);
|
|
ASSERT_EQ(p2, p3); /* same pointer */
|
|
|
|
cbm_slab_test_free(p3);
|
|
cbm_slab_destroy_thread();
|
|
PASS();
|
|
}
|
|
|
|
TEST(tier2_realloc_grows_class) {
|
|
/* realloc to larger class should copy data correctly */
|
|
cbm_vmem_init(0.5);
|
|
cbm_slab_install();
|
|
|
|
void *p = cbm_slab_test_malloc(100);
|
|
ASSERT_NOT_NULL(p);
|
|
/* Write known pattern */
|
|
for (int i = 0; i < 100; i++) {
|
|
((unsigned char *)p)[i] = (unsigned char)(i & 0xFF);
|
|
}
|
|
|
|
/* Grow to 300 — moves from class 128 to class 512 */
|
|
void *p2 = cbm_slab_test_realloc(p, 300);
|
|
ASSERT_NOT_NULL(p2);
|
|
|
|
/* Verify data was copied */
|
|
unsigned char *bytes = (unsigned char *)p2;
|
|
for (int i = 0; i < 100; i++) {
|
|
ASSERT_EQ(bytes[i], (unsigned char)(i & 0xFF));
|
|
}
|
|
|
|
/* Write to the extended area */
|
|
memset(bytes + 100, 0xFF, 200);
|
|
ASSERT_EQ(bytes[299], 0xFF);
|
|
|
|
cbm_slab_test_free(p2);
|
|
cbm_slab_destroy_thread();
|
|
PASS();
|
|
}
|
|
|
|
TEST(tier2_realloc_slab_to_tier2) {
|
|
/* realloc from Tier 1 (≤64B) to Tier 2 (>64B) */
|
|
cbm_vmem_init(0.5);
|
|
cbm_slab_install();
|
|
|
|
void *p = cbm_slab_test_malloc(32); /* Tier 1 slab */
|
|
ASSERT_NOT_NULL(p);
|
|
memset(p, 0x42, 32);
|
|
|
|
/* Promote to Tier 2 */
|
|
void *p2 = cbm_slab_test_realloc(p, 200);
|
|
ASSERT_NOT_NULL(p2);
|
|
/* First 32 bytes should be preserved */
|
|
ASSERT_EQ(((unsigned char *)p2)[0], 0x42);
|
|
ASSERT_EQ(((unsigned char *)p2)[31], 0x42);
|
|
|
|
cbm_slab_test_free(p2);
|
|
cbm_slab_destroy_thread();
|
|
PASS();
|
|
}
|
|
|
|
TEST(tier2_calloc_zeroed) {
|
|
/* calloc via tier2 must return zeroed memory */
|
|
cbm_vmem_init(0.5);
|
|
cbm_slab_install();
|
|
|
|
void *p = cbm_slab_test_calloc(1, 200);
|
|
ASSERT_NOT_NULL(p);
|
|
|
|
/* Verify all bytes are zero */
|
|
unsigned char *bytes = (unsigned char *)p;
|
|
int nonzero = 0;
|
|
for (int i = 0; i < 200; i++) {
|
|
if (bytes[i] != 0) {
|
|
nonzero++;
|
|
}
|
|
}
|
|
ASSERT_EQ(nonzero, 0);
|
|
|
|
/* Free and re-calloc — recycled memory must still be zeroed */
|
|
cbm_slab_test_free(p);
|
|
void *p2 = cbm_slab_test_calloc(1, 200);
|
|
ASSERT_NOT_NULL(p2);
|
|
bytes = (unsigned char *)p2;
|
|
nonzero = 0;
|
|
for (int i = 0; i < 200; i++) {
|
|
if (bytes[i] != 0) {
|
|
nonzero++;
|
|
}
|
|
}
|
|
ASSERT_EQ(nonzero, 0);
|
|
|
|
cbm_slab_test_free(p2);
|
|
cbm_slab_destroy_thread();
|
|
PASS();
|
|
}
|
|
|
|
TEST(tier2_mixed_alloc_free_stress) {
|
|
/* Stress test: interleaved allocs and frees across Tier 1 and Tier 2 */
|
|
cbm_vmem_init(0.5);
|
|
cbm_slab_install();
|
|
|
|
void *ptrs[100];
|
|
size_t sizes[100];
|
|
|
|
/* Allocate 100 blocks of varying sizes */
|
|
for (int i = 0; i < 100; i++) {
|
|
sizes[i] = (size_t)(16 + (i * 47) % 4000); /* 16..4000 */
|
|
ptrs[i] = cbm_slab_test_malloc(sizes[i]);
|
|
ASSERT_NOT_NULL(ptrs[i]);
|
|
memset(ptrs[i], (unsigned char)(i & 0xFF), sizes[i]);
|
|
}
|
|
|
|
/* Free odd-indexed blocks */
|
|
for (int i = 1; i < 100; i += 2) {
|
|
cbm_slab_test_free(ptrs[i]);
|
|
ptrs[i] = NULL;
|
|
}
|
|
|
|
/* Re-allocate freed slots with different sizes */
|
|
for (int i = 1; i < 100; i += 2) {
|
|
sizes[i] = (size_t)(32 + (i * 31) % 2000);
|
|
ptrs[i] = cbm_slab_test_malloc(sizes[i]);
|
|
ASSERT_NOT_NULL(ptrs[i]);
|
|
memset(ptrs[i], (unsigned char)((i + 1) & 0xFF), sizes[i]);
|
|
}
|
|
|
|
/* Verify even-indexed blocks still have original data */
|
|
for (int i = 0; i < 100; i += 2) {
|
|
ASSERT_EQ(((unsigned char *)ptrs[i])[0], (unsigned char)(i & 0xFF));
|
|
}
|
|
|
|
/* Free all */
|
|
for (int i = 0; i < 100; i++) {
|
|
cbm_slab_test_free(ptrs[i]);
|
|
}
|
|
|
|
cbm_slab_destroy_thread();
|
|
PASS();
|
|
}
|
|
|
|
/* ── Slab + vmem parallel extraction test ──────────────────── */
|
|
|
|
static char g_vmem_tmpdir[256];
|
|
|
|
static int setup_vmem_test_repo(void) {
|
|
snprintf(g_vmem_tmpdir, sizeof(g_vmem_tmpdir), "/tmp/cbm_vmem_XXXXXX");
|
|
if (!mkdtemp(g_vmem_tmpdir)) {
|
|
return -1;
|
|
}
|
|
|
|
char path[512];
|
|
|
|
/* Create multiple Go files to force multi-file parallel extraction.
|
|
* We need enough files to exercise slab reset between files on a worker. */
|
|
for (int i = 0; i < 6; i++) {
|
|
snprintf(path, sizeof(path), "%s/file%d.go", g_vmem_tmpdir, i);
|
|
FILE *f = fopen(path, "w");
|
|
if (!f) {
|
|
return -1;
|
|
}
|
|
fprintf(f,
|
|
"package main\n\nfunc F%d() {\n\tprintln(\"hello\")\n}\n\n"
|
|
"func G%d() int {\n\treturn F%d() + %d\n}\n",
|
|
i, i, i, i);
|
|
fclose(f);
|
|
}
|
|
|
|
/* Add a C file to exercise the preprocessor second-pass path */
|
|
snprintf(path, sizeof(path), "%s/util.c", g_vmem_tmpdir);
|
|
FILE *f = fopen(path, "w");
|
|
if (!f) {
|
|
return -1;
|
|
}
|
|
fprintf(f, "#include <stdio.h>\nvoid util_func(void) { printf(\"hi\"); }\n"
|
|
"int util_add(int a, int b) { return a + b; }\n");
|
|
fclose(f);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void teardown_vmem_test_repo(void) {
|
|
if (g_vmem_tmpdir[0]) {
|
|
th_rmtree(g_vmem_tmpdir);
|
|
g_vmem_tmpdir[0] = '\0';
|
|
}
|
|
}
|
|
|
|
TEST(vmem_parallel_extract_with_slab) {
|
|
/* This test reproduces a SIGSEGV that occurred when:
|
|
* 1. vmem is active (arena + source via mmap)
|
|
* 2. slab allocator is installed (tree-sitter uses slab)
|
|
* 3. slab_reset_thread() was called between files, corrupting
|
|
* the parser's live slab-allocated internal state (subtree pool,
|
|
* stack entries, cached tokens).
|
|
*
|
|
* The fix: don't call slab_reset_thread() between files. Normal
|
|
* slab_free() from ts_tree_delete() returns chunks for reuse.
|
|
* slab_destroy_thread() reclaims everything on worker exit. */
|
|
cbm_vmem_init(0.5);
|
|
|
|
if (setup_vmem_test_repo() != 0) {
|
|
FAIL("tmpdir setup failed");
|
|
}
|
|
|
|
cbm_discover_opts_t opts = {.mode = CBM_MODE_FULL};
|
|
cbm_file_info_t *files = NULL;
|
|
int file_count = 0;
|
|
if (cbm_discover(g_vmem_tmpdir, &opts, &files, &file_count) != 0) {
|
|
teardown_vmem_test_repo();
|
|
FAIL("discover failed");
|
|
}
|
|
|
|
ASSERT_GTE(file_count, 5);
|
|
|
|
cbm_gbuf_t *gbuf = cbm_gbuf_new("vmem-test", g_vmem_tmpdir);
|
|
cbm_registry_t *reg = cbm_registry_new();
|
|
atomic_int cancelled;
|
|
atomic_init(&cancelled, 0);
|
|
|
|
cbm_pipeline_ctx_t ctx = {
|
|
.project_name = "vmem-test",
|
|
.repo_path = g_vmem_tmpdir,
|
|
.gbuf = gbuf,
|
|
.registry = reg,
|
|
.cancelled = &cancelled,
|
|
};
|
|
|
|
_Atomic int64_t shared_ids;
|
|
int64_t gbuf_next = cbm_gbuf_next_id(gbuf);
|
|
atomic_init(&shared_ids, gbuf_next);
|
|
|
|
CBMFileResult **result_cache = calloc(file_count, sizeof(CBMFileResult *));
|
|
ASSERT_NOT_NULL(result_cache);
|
|
|
|
/* Run parallel extraction with 2 workers — enough to trigger
|
|
* multi-file slab reuse on at least one worker. */
|
|
int rc = cbm_parallel_extract(&ctx, files, file_count, result_cache, &shared_ids, 2);
|
|
ASSERT_EQ(rc, 0);
|
|
|
|
/* Verify extraction produced results */
|
|
int cached_count = 0;
|
|
for (int i = 0; i < file_count; i++) {
|
|
if (result_cache[i]) {
|
|
cached_count++;
|
|
}
|
|
}
|
|
ASSERT_GTE(cached_count, 5);
|
|
|
|
/* Verify nodes were created */
|
|
ASSERT_GT(cbm_gbuf_node_count(gbuf), 0);
|
|
|
|
/* Clean up */
|
|
for (int i = 0; i < file_count; i++) {
|
|
if (result_cache[i]) {
|
|
cbm_free_result(result_cache[i]);
|
|
}
|
|
}
|
|
free(result_cache);
|
|
cbm_registry_free(reg);
|
|
cbm_gbuf_free(gbuf);
|
|
cbm_discover_free(files, file_count);
|
|
teardown_vmem_test_repo();
|
|
PASS();
|
|
}
|
|
|
|
SUITE(vmem) {
|
|
RUN_TEST(vmem_budget_zero_before_init);
|
|
RUN_TEST(vmem_alloc_and_free);
|
|
RUN_TEST(vmem_alloc_zero_returns_zeroed);
|
|
RUN_TEST(vmem_budget_tracking);
|
|
RUN_TEST(vmem_peak_tracks);
|
|
RUN_TEST(vmem_worker_budget);
|
|
/* Edge cases and resource management */
|
|
RUN_TEST(vmem_alloc_zero_returns_null);
|
|
RUN_TEST(vmem_free_null_zero_no_crash);
|
|
RUN_TEST(vmem_free_null_nonzero_no_crash);
|
|
RUN_TEST(vmem_alloc_very_large);
|
|
RUN_TEST(vmem_sequential_alloc_free_no_leak);
|
|
RUN_TEST(vmem_worker_budget_negative_workers);
|
|
RUN_TEST(vmem_over_budget_when_nothing_allocated);
|
|
RUN_TEST(vmem_allocated_tracks_alloc_free_cycle);
|
|
RUN_TEST(vmem_multiple_alloc_tracks_cumulative);
|
|
RUN_TEST(vmem_peak_never_decreases);
|
|
RUN_TEST(vmem_worker_budget_one_worker);
|
|
RUN_TEST(vmem_worker_budget_many_workers);
|
|
/* Arena-vmem integration */
|
|
RUN_TEST(arena_vmem_alloc_and_destroy);
|
|
RUN_TEST(arena_vmem_grow_tracks_sizes);
|
|
RUN_TEST(arena_vmem_large_alloc);
|
|
RUN_TEST(arena_vmem_reset_frees_blocks);
|
|
/* Tier 2 slab allocator tests */
|
|
RUN_TEST(tier2_alloc_and_free_128);
|
|
RUN_TEST(tier2_alloc_all_classes);
|
|
RUN_TEST(tier2_free_list_reuse);
|
|
RUN_TEST(tier2_oversized_dedicated);
|
|
RUN_TEST(tier2_realloc_same_class);
|
|
RUN_TEST(tier2_realloc_grows_class);
|
|
RUN_TEST(tier2_realloc_slab_to_tier2);
|
|
RUN_TEST(tier2_calloc_zeroed);
|
|
RUN_TEST(tier2_mixed_alloc_free_stress);
|
|
/* Integration */
|
|
RUN_TEST(vmem_parallel_extract_with_slab);
|
|
}
|