Skip to content
Draft
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
10 changes: 10 additions & 0 deletions .github/workflows/build-kbox.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,16 @@ jobs:
- name: Run unit tests (ASAN/UBSAN)
run: make check-unit

# ---- Perf tests: no sanitizers, only perf benchmarks ----
perf-tests:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Run perf tests
run: make check-perf

# ---- Build kbox + prepare rootfs ----
build-kbox:
runs-on: ubuntu-24.04
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ __pycache__
*.ext4
/kbox
tests/unit/test-runner
tests/unit/test-perf
deps/
lkl-x86_64/
lkl-aarch64/
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ KCONFIG_CONF := configs/Kconfig

# Targets that don't require .config
CONFIG_TARGETS := config defconfig oldconfig savedefconfig clean distclean indent \
check-unit check-syntax check-commitlog fetch-lkl build-lkl \
check-unit check-perf check-syntax check-commitlog fetch-lkl build-lkl \
fetch-minislirp install-hooks guest-bins stress-bins rootfs
CONFIG_GENERATORS := config defconfig oldconfig

Expand Down
19 changes: 17 additions & 2 deletions mk/tests.mk
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
TEST_DIR = tests/unit
TEST_SRCS = $(TEST_DIR)/test-runner.c \
$(TEST_DIR)/test-fd-table.c \
$(TEST_DIR)/test-fd-table-refcount.c \
$(TEST_DIR)/test-path.c \
$(TEST_DIR)/test-mount.c \
$(TEST_DIR)/test-cli.c \
Expand Down Expand Up @@ -55,6 +56,7 @@ TEST_SUPPORT_SRCS += $(SRC_DIR)/rewrite.c \
endif

TEST_TARGET = tests/unit/test-runner
PERF_TARGET = tests/unit/test-perf

# Guest test programs (compiled statically, run inside kbox)
GUEST_DIR = tests/guest
Expand All @@ -71,7 +73,7 @@ ROOTFS = alpine.ext4

# ---- Test targets ----

check: check-unit check-integration check-stress
check: check-unit check-perf check-integration check-stress

check-unit: $(TEST_TARGET)
@echo " RUN check-unit"
Expand All @@ -85,6 +87,19 @@ $(TEST_TARGET): $(TEST_SRCS) $(TEST_SUPPORT_SRCS) $(wildcard .config)
@echo " LD $@"
$(Q)$(CC) $(CFLAGS) -DKBOX_UNIT_TEST -o $@ $(TEST_SRCS) $(TEST_SUPPORT_SRCS) $(TEST_LDFLAGS) -lpthread

check-perf: $(PERF_TARGET)
@echo " RUN check-perf"
$(Q)./$(PERF_TARGET)

# Perf-test binary: strip debug/sanitizer flags from CFLAGS, force -O2.
PERF_TEST_CFLAGS := $(filter-out -O0 -O1 -O3 -g -g3 -fsanitize% -fno-omit-frame-pointer,$(CFLAGS)) \
-Wno-unused-function -O2 -DKBOX_UNIT_TEST -DKBOX_PERF_TESTS -DKBOX_PERF_ONLY
PERF_TEST_LDFLAGS := $(filter-out -L$(LKL_DIR) -L$(LKL_DIR)/lib -fsanitize%,$(LDFLAGS))

$(PERF_TARGET): $(TEST_SRCS) $(TEST_SUPPORT_SRCS) $(wildcard .config)
@echo " LD $@"
$(Q)$(CC) $(PERF_TEST_CFLAGS) -o $@ $(TEST_SRCS) $(TEST_SUPPORT_SRCS) $(PERF_TEST_LDFLAGS) -lpthread

check-integration: $(TARGET) guest-bins stress-bins $(ROOTFS)
@echo " RUN check-integration"
$(Q)./scripts/run-tests.sh ./$(TARGET) $(ROOTFS)
Expand Down Expand Up @@ -163,4 +178,4 @@ check-commitlog:
@echo " RUN check-commitlog"
$(Q)scripts/check-commitlog.sh

.PHONY: check check-unit check-integration check-stress check-commitlog guest-bins stress-bins rootfs check-syntax
.PHONY: check check-unit check-perf check-integration check-stress check-commitlog guest-bins stress-bins rootfs check-syntax
29 changes: 25 additions & 4 deletions src/fd-table.c
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ static inline void rev_host_set(struct kbox_fd_table *t, long host_fd, long vfd)

if (host_fd < 0 || (uint64_t) host_fd >= KBOX_HOST_FD_REVERSE_MAX)
return;

t->host_fd_refs[host_fd]++;
cur = t->host_to_vfd[host_fd];
if (cur == KBOX_HOST_VFD_NONE || cur == (int32_t) vfd) {
t->host_to_vfd[host_fd] = (int32_t) vfd;
Expand All @@ -116,18 +118,35 @@ static inline void rev_host_clear(struct kbox_fd_table *t,
long host_fd,
long vfd)
{
if (host_fd < 0 || (uint64_t) host_fd >= KBOX_HOST_FD_REVERSE_MAX)
if (host_fd < 0 || (uint64_t) host_fd >= KBOX_HOST_FD_REVERSE_MAX ||
t->host_fd_refs[host_fd] == 0)
return;
/* Only the single-holder case can be cleared authoritatively. If
* the slot is MULTI, leave it: we cannot prove this is the last
* holder without scanning, and the slow path will handle later
* lookups correctly. If the slot is NONE or claims a different
* vfd, we were not the indexed holder; nothing to do.
*/
if (t->host_to_vfd[host_fd] == (int32_t) vfd)

t->host_fd_refs[host_fd]--;
if (t->host_fd_refs[host_fd] == 0) {
t->host_to_vfd[host_fd] = KBOX_HOST_VFD_NONE;
}
} else if (t->host_fd_refs[host_fd] == 1) {
/* 1. Try a normal search first */
long cur_vfd = kbox_fd_table_find_by_host_fd(t, host_fd);

/* 2. If the search tripped over the dying slot, hide it and search
* again */
if (cur_vfd == vfd) {
struct kbox_fd_entry *e = fd_lookup(t, vfd);
e->host_fd = -1;
cur_vfd = kbox_fd_table_find_by_host_fd(t, host_fd);
e->host_fd = host_fd;
}

t->host_to_vfd[host_fd] = cur_vfd;
}
}
static inline void lkl_ref_inc(struct kbox_fd_table *t, long lkl_fd)
{
if (lkl_fd >= 0 && (uint64_t) lkl_fd < KBOX_LKL_FD_REFMAX &&
Expand All @@ -154,8 +173,10 @@ void kbox_fd_table_init(struct kbox_fd_table *t)
clear_fd_entry(&t->low_fds[i]);
for (i = 0; i < KBOX_MID_FD_MAX; i++)
clear_fd_entry(&t->mid_fds[i]);
for (i = 0; i < KBOX_HOST_FD_REVERSE_MAX; i++)
for (i = 0; i < KBOX_HOST_FD_REVERSE_MAX; i++) {
t->host_to_vfd[i] = KBOX_HOST_VFD_NONE;
t->host_fd_refs[i] = 0;
}
for (i = 0; i < KBOX_LKL_FD_REFMAX; i++)
t->lkl_fd_refs[i] = 0;
t->next_fd = KBOX_FD_BASE;
Expand Down
4 changes: 4 additions & 0 deletions src/fd-table.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ struct kbox_fd_table {
* kbox_fd_table_find_by_host_fd() on the close() hot path.
*/
int32_t host_to_vfd[KBOX_HOST_FD_REVERSE_MAX];
/* Tracks how many VFDs point to a specific Host FD.
* This allows us to know when to downgrade from MULTI.
*/
uint16_t host_fd_refs[KBOX_HOST_FD_REVERSE_MAX];
/* Refcount: how many virtual fds currently reference each
* lkl_fd. Replaces the O(n) lkl_fd_has_other_ref scan and the
* still_ref loop in forward_close.
Expand Down
123 changes: 123 additions & 0 deletions tests/unit/test-fd-table-refcount.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/* SPDX-License-Identifier: MIT */
#include <string.h>

#include "fd-table.h"
#include "test-runner.h"
#define KBOX_HOST_VFD_NONE ((int32_t) -1)
#define KBOX_HOST_VFD_MULTI ((int32_t) -2)

static void test_fd_table_multi_downgrade_regression(void)
{
struct kbox_fd_table t;
long vfd1, vfd2;
long host_fd = 42;

kbox_fd_table_init(&t);

vfd1 = kbox_fd_table_insert(&t, 10, 0);
kbox_fd_table_set_host_fd(&t, vfd1, host_fd);

ASSERT_EQ(t.host_to_vfd[host_fd], vfd1);
ASSERT_EQ(t.host_fd_refs[host_fd], 1);

/* MULTI state*/
vfd2 = kbox_fd_table_insert(&t, 20, 0);
kbox_fd_table_set_host_fd(&t, vfd2, host_fd);

ASSERT_EQ(t.host_to_vfd[host_fd], KBOX_HOST_VFD_MULTI);
ASSERT_EQ(t.host_fd_refs[host_fd], 2);

/* Downgrade back to Single*/
kbox_fd_table_remove(&t, vfd2);

ASSERT_EQ(t.host_to_vfd[host_fd], vfd1);
ASSERT_EQ(t.host_fd_refs[host_fd], 1);

/* Should return the exact vfd */
ASSERT_EQ(kbox_fd_table_find_by_host_fd(&t, host_fd), vfd1);
}

#ifdef KBOX_PERF_TESTS
#include <stdio.h>
#include <time.h>
#define PERF_ITERATIONS 1000000
static double time_diff_ns(struct timespec *start, struct timespec *end)
{
return ((double) (end->tv_sec - start->tv_sec) * 1e9) +
(double) (end->tv_nsec - start->tv_nsec);
}

static void test_fd_table_o1_characteristics(void)
{
struct kbox_fd_table t;
int ns_to_test[] = {64, 256, 1024, 4096, 16384};
int num_sizes = sizeof(ns_to_test) / sizeof(ns_to_test[0]);

double baseline_present_ns = 0;
double baseline_absent_ns = 0;

printf("\n--- O(1) Characteristic Perf Test ---\n");

for (int i = 0; i < num_sizes; i++) {
int n = ns_to_test[i];
kbox_fd_table_init(&t);
long target_present = 0;
long target_absent = 65535;

for (long j = 0, host_fd = 0; j < n; j++) {
long vfd = j;
kbox_fd_table_insert_at(&t, vfd, j, 0);

/* linear congruential generator to prevent caching */
host_fd = (16645 * host_fd + 10139) % 65536;
kbox_fd_table_set_host_fd(&t, vfd, host_fd);
if (j == n / 2) {
target_present = host_fd;
}
}

struct timespec start, end;
double present_time_ns, absent_time_ns;

long sum_present = 0;
long sum_absent = 0;

clock_gettime(CLOCK_MONOTONIC, &start);
for (int k = 0; k < PERF_ITERATIONS; k++) {
sum_present += kbox_fd_table_find_by_host_fd(&t, target_present);
}
clock_gettime(CLOCK_MONOTONIC, &end);
present_time_ns = time_diff_ns(&start, &end) / PERF_ITERATIONS;

clock_gettime(CLOCK_MONOTONIC, &start);
for (int k = 0; k < PERF_ITERATIONS; k++) {
sum_absent += kbox_fd_table_find_by_host_fd(&t, target_absent);
}
clock_gettime(CLOCK_MONOTONIC, &end);
absent_time_ns = time_diff_ns(&start, &end) / PERF_ITERATIONS;

printf("N = %-5d | Present: %6.2f ns/op | Absent: %6.2f ns/op\n", n,
present_time_ns, absent_time_ns);

if (i == 0) {
baseline_present_ns = present_time_ns;
baseline_absent_ns = absent_time_ns;
} else {
/* The per-lookup cost ratio across N must stay under ~2x */
double present_ratio = present_time_ns / baseline_present_ns;
double absent_ratio = absent_time_ns / baseline_absent_ns;

ASSERT_TRUE(present_ratio < 2);
ASSERT_TRUE(absent_ratio < 2);
}
}
}
#endif
void test_fd_table_refcount_init(void)
{
TEST_REGISTER(test_fd_table_multi_downgrade_regression);

#ifdef KBOX_PERF_TESTS
PERF_REGISTER(test_fd_table_o1_characteristics);
#endif
}
2 changes: 2 additions & 0 deletions tests/unit/test-runner.c
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ void test_pass(void)
/* External init functions from each test file */
/* Portable test suites (all hosts) */
extern void test_fd_table_init(void);
extern void test_fd_table_refcount_init(void);
extern void test_path_init(void);
extern void test_mount_init(void);
extern void test_cli_init(void);
Expand Down Expand Up @@ -120,6 +121,7 @@ int main(int argc, char *argv[])

/* Portable suites */
test_fd_table_init();
test_fd_table_refcount_init();
test_path_init();
test_mount_init();
test_cli_init();
Expand Down
5 changes: 5 additions & 0 deletions tests/unit/test-runner.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ void test_pass(void);
} while (0)
#endif

#ifdef KBOX_PERF_ONLY
#define TEST_REGISTER(fn) ((void) 0)
#define PERF_REGISTER(fn) test_register(#fn, fn)
#else
#define TEST_REGISTER(fn) test_register(#fn, fn)
#endif

#endif /* TEST_RUNNER_H */
Loading