Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,9 @@ if(KVSPACE_BUILD_TESTS)
target_include_directories(test_shm_split PRIVATE src)
target_link_libraries(test_shm_split PRIVATE kvspace-c)
add_test(NAME shm_split COMMAND test_shm_split)

add_executable(test_art_scan tests/test_art_scan.c)
target_include_directories(test_art_scan PRIVATE src)
target_link_libraries(test_art_scan PRIVATE kvspace-c)
add_test(NAME art_scan COMMAND test_art_scan)
endif()
52 changes: 48 additions & 4 deletions src/kvspace.c
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,42 @@ static char *edir(const char *p) {
r[l + 1] = '\0';
return r;
}
/* Covering node for pfx: *bpos is reconstructed length at arrival. */
static int32_t art_cover(kvspace_t *kv, int32_t nid, const uint8_t *pfx,
int plen, char *buf, int bcap, int *bpos) {
int d = 0;
if (nid < 0 || !pfx || plen < 0 || !buf || !bpos)
return -1;
while (nid >= 0) {
art_hdr_t *h = art_hdr(kv, nid);
if (!h)
return -1;
int pd = d, cover = 0;
if (h->prefix_len) {
int s = pfx_shared(h->prefix, h->prefix_len, pfx + d, plen - d);
if (s < h->prefix_len) {
if (d + s < plen)
return -1;
cover = 1;
} else
d += h->prefix_len;
}
if (cover || d == plen) {
if (pd >= bcap)
return -1;
if (pd)
memcpy(buf, pfx, (size_t)pd);
*bpos = pd;
return nid;
}
nid = art_child(kv, h, pfx[d]);
if (nid < 0)
return -1;
d++;
}
return -1;
}

/* ---- prefix scan: collect all keys under prefix into out[0..*n-1] ---- */
static void art_scan(kvspace_t *kv, int32_t nid, char *buf, int bpos, int bcap,
const char *pfx, int plen, char ***out, int32_t *n) {
Expand Down Expand Up @@ -749,6 +785,16 @@ static void art_scan(kvspace_t *kv, int32_t nid, char *buf, int bpos, int bcap,
}
}

static void art_scan_pfx(kvspace_t *kv, const char *pfx, int plen, char *buf,
int bcap, char ***out, int32_t *n) {
int bpos = 0;
int32_t nid = art_cover(kv, kv->hdr->art_root, (const uint8_t *)pfx, plen,
buf, bcap, &bpos);
if (nid < 0)
return;
art_scan(kv, nid, buf, bpos, bcap, pfx, plen, out, n);
}

/* ============ lifecycle ============ */
/* 8 * 64^k; s wraps to 0. */
static bool sbo_data_size_ok(size_t n) {
Expand Down Expand Up @@ -1932,8 +1978,7 @@ int kvspaceShmList(kvspace_t *kv, const char *prefix, bool ex, int resolve,
memset(buf, 0, sizeof(buf));
if (kv->hdr->art_root < 0)
return 0;
art_scan(kv, kv->hdr->art_root, buf, 0, (int)sizeof(buf), pfx, plen, &out,
&n);
art_scan_pfx(kv, pfx, plen, buf, (int)sizeof(buf), &out, &n);
// filter: only direct children (one level below prefix)
char **filt = malloc(sizeof(char *) * n);
int32_t fn = 0;
Expand Down Expand Up @@ -1973,8 +2018,7 @@ int kvspaceShmList(kvspace_t *kv, const char *prefix, bool ex, int resolve,
char ebuf[2048];
memset(ebuf, 0, sizeof ebuf);
int el = (int)strlen(extpath);
art_scan(kv, kv->hdr->art_root, ebuf, 0, (int)sizeof ebuf, extpath, el,
&eo, &en);
art_scan_pfx(kv, extpath, el, ebuf, (int)sizeof ebuf, &eo, &en);
filt = realloc(filt, sizeof(char *) * (size_t)(fn + en));
for (int i = 0; i < en; i++) {
const char *k = eo[i];
Expand Down
240 changes: 240 additions & 0 deletions tests/test_art_scan.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
/* test_art_scan — prefix List/DelTree/CpTree scan the covering subtree (#267) */

#define _GNU_SOURCE
#include "kvspace_shm.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#define SBO_DATA_SIZE (8UL * 64 * 64 * 64 * 64)

static int failures = 0;
#define CHECK(cond, ...) \
do { \
if (!(cond)) { \
failures++; \
fprintf(stderr, " FAIL %s:%d: ", __FILE__, __LINE__); \
fprintf(stderr, __VA_ARGS__); \
fprintf(stderr, "\n"); \
} \
} while (0)
#define REQUIRE(cond, ...) \
do { \
CHECK(cond, __VA_ARGS__); \
if (!(cond)) \
return; \
} while (0)

static double now_s(void) {
struct timespec t;
clock_gettime(CLOCK_MONOTONIC, &t);
return (double)t.tv_sec + (double)t.tv_nsec * 1e-9;
}

static int set_int32(kvspace_t *kv, const char *key, int32_t v) {
uint8_t raw[4] = {(uint8_t)v, (uint8_t)(v >> 8), (uint8_t)(v >> 16),
(uint8_t)(v >> 24)};
uint8_t *tlv;
int32_t n = kvspaceXvalueEncode(KVSPACE_KIND_INT32, raw, 4, NULL, 0, &tlv);
int rc = kvspaceShmSet(kv, key, tlv, n);
free(tlv);
return rc;
}

static int32_t get_int32(kvspace_t *kv, const char *key) {
int32_t len = 0;
uint8_t *d = kvspaceShmGet(kv, key, 1, &len);
if (!d || len <= 0)
return -1;
xvalue_head_t h = kvspaceXvalueDecodeHead(d, len);
return h.raw_len == 4 ? kvspaceXvalueRawInt32(h.raw) : -1;
}

static void list_free(char **ns, int32_t n) {
if (!ns)
return;
for (int32_t i = 0; i < n; i++)
free(ns[i]);
free(ns);
}

static int list_has(char **ns, int32_t n, const char *s) {
for (int32_t i = 0; i < n; i++)
if (strcmp(ns[i], s) == 0)
return 1;
return 0;
}

static int do_list(kvspace_t *kv, const char *pfx, char ***on, int32_t *n) {
*on = NULL;
*n = 0;
return kvspaceShmList(kv, pfx, false, 0, on, n);
}

static void t_correct(const char *db) {
printf("[correct] isolation, nested, compressed prefix, miss, deltree, cptree\n");
kvspace_t *kv = kvspaceShmOpen(db, SBO_DATA_SIZE);
REQUIRE(kv != NULL, "open failed");

CHECK(set_int32(kv, "/lib/a", 1) == 0, "set /lib/a");
CHECK(set_int32(kv, "/lib/b", 2) == 0, "set /lib/b");
CHECK(set_int32(kv, "/lib/mod/x", 3) == 0, "set /lib/mod/x");
CHECK(set_int32(kv, "/d/f/p", 4) == 0, "set /d/f/p");
CHECK(set_int32(kv, "/d/f/q", 5) == 0, "set /d/f/q");
CHECK(set_int32(kv, "/d/g", 6) == 0, "set /d/g");
CHECK(set_int32(kv, "/foo/bar1", 7) == 0, "set /foo/bar1");
CHECK(set_int32(kv, "/foo/bar2", 8) == 0, "set /foo/bar2");
CHECK(set_int32(kv, "/foo/zzz", 9) == 0, "set /foo/zzz");

char **ns;
int32_t n;

REQUIRE(do_list(kv, "/d/", &ns, &n) == 0, "list /d/");
CHECK(n == 2, "list /d/ count %d", n);
CHECK(list_has(ns, n, "f") && list_has(ns, n, "g"), "list /d/ names");
list_free(ns, n);

REQUIRE(do_list(kv, "/d/f/", &ns, &n) == 0, "list /d/f/");
CHECK(n == 2, "list /d/f/ count %d", n);
CHECK(list_has(ns, n, "p") && list_has(ns, n, "q"), "list /d/f/ names");
list_free(ns, n);

REQUIRE(do_list(kv, "/lib/", &ns, &n) == 0, "list /lib/");
CHECK(n == 3, "list /lib/ count %d", n);
CHECK(list_has(ns, n, "a") && list_has(ns, n, "b") && list_has(ns, n, "mod"),
"list /lib/ names");
list_free(ns, n);

REQUIRE(do_list(kv, "/foo/", &ns, &n) == 0, "list /foo/");
CHECK(n == 3, "list /foo/ count %d", n);
CHECK(list_has(ns, n, "bar1") && list_has(ns, n, "bar2") &&
list_has(ns, n, "zzz"),
"list /foo/ names");
list_free(ns, n);

REQUIRE(do_list(kv, "/nope/", &ns, &n) == 0, "list /nope/");
CHECK(n == 0, "list /nope/ count %d", n);
list_free(ns, n);

REQUIRE(do_list(kv, "/food/", &ns, &n) == 0, "list /food/");
CHECK(n == 0, "list /food/ count %d", n);
list_free(ns, n);

REQUIRE(do_list(kv, "/", &ns, &n) == 0, "list /");
CHECK(list_has(ns, n, "lib") && list_has(ns, n, "d") && list_has(ns, n, "foo"),
"list / names");
list_free(ns, n);

CHECK(kvspaceShmExtindex(kv, "/e/", "/lib/") == 0, "extindex");
REQUIRE(kvspaceShmList(kv, "/e/", true, 0, &ns, &n) == 0, "list /e/ ex");
CHECK(list_has(ns, n, "a") && list_has(ns, n, "b") && list_has(ns, n, "mod"),
"extindex children");
list_free(ns, n);

CHECK(kvspaceShmCptree(kv, "/d/f", "/cp") == 0, "cptree");
REQUIRE(do_list(kv, "/cp/", &ns, &n) == 0, "list /cp/");
CHECK(n == 2 && list_has(ns, n, "p") && list_has(ns, n, "q"),
"cptree children");
list_free(ns, n);
CHECK(get_int32(kv, "/cp/p") == 4 && get_int32(kv, "/cp/q") == 5,
"cptree values");

CHECK(kvspaceShmDeltree(kv, "/d") == 0, "deltree /d");
REQUIRE(do_list(kv, "/d/", &ns, &n) == 0, "list /d/ after deltree");
CHECK(n == 0, "deltree left children %d", n);
list_free(ns, n);
CHECK(get_int32(kv, "/lib/a") == 1 && get_int32(kv, "/foo/bar1") == 7,
"deltree escaped sibling trees");
CHECK(get_int32(kv, "/d/f/p") == -1, "deltree did not remove target");

kvspaceShmClose(kv);
}

static int fill_noise(kvspace_t *kv, int from, int to) {
char key[32];
int fail = 0;
for (int i = from; i < to; i++) {
snprintf(key, sizeof key, "/n/%02d/%03d", i / 100, i % 100);
fail += set_int32(kv, key, i) != 0;
}
return fail;
}

static double time_list(kvspace_t *kv, const char *pfx, int reps) {
for (int i = 0; i < 20; i++) {
char **ns;
int32_t n;
do_list(kv, pfx, &ns, &n);
list_free(ns, n);
}
double t0 = now_s();
for (int i = 0; i < reps; i++) {
char **ns;
int32_t n;
do_list(kv, pfx, &ns, &n);
list_free(ns, n);
}
return now_s() - t0;
}

static void t_scale(const char *db) {
const int lo = 1000, hi = 8000, reps = 400;
printf("[scale] List(/t/) vs sibling tree size %d -> %d, %d reps\n", lo, hi,
reps);
kvspace_t *kv = kvspaceShmOpen(db, SBO_DATA_SIZE);
REQUIRE(kv != NULL, "open failed");

CHECK(set_int32(kv, "/t/a", 1) == 0, "set /t/a");
CHECK(set_int32(kv, "/t/b", 2) == 0, "set /t/b");
CHECK(set_int32(kv, "/t/c/x", 3) == 0, "set /t/c/x");
CHECK(fill_noise(kv, 0, lo) == 0, "fill lo noise");

char **ns;
int32_t n;
REQUIRE(do_list(kv, "/t/", &ns, &n) == 0, "list /t/");
CHECK(n == 3 && list_has(ns, n, "a") && list_has(ns, n, "b") &&
list_has(ns, n, "c"),
"list /t/ names");
list_free(ns, n);

double t_lo = time_list(kv, "/t/", reps);
CHECK(fill_noise(kv, lo, hi) == 0, "fill hi noise");
REQUIRE(do_list(kv, "/t/", &ns, &n) == 0, "list /t/ after noise");
CHECK(n == 3, "list /t/ count after noise %d", n);
list_free(ns, n);
double t_hi = time_list(kv, "/t/", reps);
double ratio = t_lo > 0 ? t_hi / t_lo : 0;

printf(" list /t/ %d noise: %.4fs\n", lo, t_lo);
printf(" list /t/ %d noise: %.4fs ratio=%.2f (must not track noise)\n", hi,
t_hi, ratio);
CHECK(ratio < 2.5, "List(/t/) scaled with sibling size: %.2fx", ratio);

kvspaceShmClose(kv);
}

int main(int argc, char **argv) {
char tmpl[] = "/tmp/kvspace-artscan-XXXXXX";
const char *dir = argc > 1 ? argv[1] : mkdtemp(tmpl);
if (!dir) {
perror("mkdtemp");
return 2;
}
char db[512], db2[512];
snprintf(db, sizeof db, "%s/db", dir);
snprintf(db2, sizeof db2, "%s/db2", dir);

t_correct(db);
t_scale(db2);

if (argc <= 1) {
char cmd[700];
snprintf(cmd, sizeof cmd, "rm -rf '%s'", dir);
if (system(cmd) != 0)
fprintf(stderr, "cleanup failed: %s\n", dir);
}
printf(failures ? "FAILED (%d)\n" : "OK\n", failures);
return failures ? 1 : 0;
}
Loading