From eeed4c376d8a90f1004ba527b2406291efded73a Mon Sep 17 00:00:00 2001 From: GigabiteStudios Date: Wed, 17 Jun 2026 10:44:20 -0500 Subject: [PATCH] perf(io): reduce file path overhead and add benchmarks --- demo/unit_test.c | 153 ++++++++++++++++++++++++++++++++++++++++++++- src/ikv.c | 43 +++++++++---- src/loaders/ikv2.c | 22 +------ 3 files changed, 182 insertions(+), 36 deletions(-) diff --git a/demo/unit_test.c b/demo/unit_test.c index 9ef482f..e9cd708 100644 --- a/demo/unit_test.c +++ b/demo/unit_test.c @@ -5,6 +5,9 @@ #include #include #include +#ifdef _WIN32 +#include +#endif #ifdef IKV_TESTING #include "../src/internal/ikv_internal.h" @@ -61,6 +64,63 @@ static int expect_string(test_context_t *context, const char *actual, const char return 0; } +static double benchmark_now_us(void) +{ +#ifdef _WIN32 + static LARGE_INTEGER frequency; + static int initialized = 0; + LARGE_INTEGER counter; + + if (!initialized) + { + QueryPerformanceFrequency(&frequency); + initialized = 1; + } + + QueryPerformanceCounter(&counter); + return ((double)counter.QuadPart * 1000000.0) / (double)frequency.QuadPart; +#else + struct timespec ts; + timespec_get(&ts, TIME_UTC); + return ((double)ts.tv_sec * 1000000.0) + ((double)ts.tv_nsec / 1000.0); +#endif +} + +static ikv_node_t *create_benchmark_root(void) +{ + ikv_node_t *root = ikv_create_object("benchmark"); + ikv_node_t *inventory = NULL; + ikv_node_t *stats = NULL; + + if (!root) + return NULL; + + ikv_object_set_string(root, "title", "benchmark payload"); + ikv_object_set_int(root, "count", 1337); + ikv_object_set_bool(root, "enabled", true); + ikv_object_set_float(root, "ratio", 42.5); + + inventory = ikv_object_add_array(root, "inventory", IKV_STRING); + stats = ikv_object_add_object(root, "stats"); + if (!inventory || !stats) + { + ikv_free(root); + return NULL; + } + + ikv_array_add_string(inventory, "wrench"); + ikv_array_add_string(inventory, "battery"); + ikv_array_add_string(inventory, "map"); + ikv_array_add_string(inventory, "radio"); + ikv_array_add_string(inventory, "flares"); + + ikv_object_set_int(stats, "health", 100); + ikv_object_set_int(stats, "armor", 75); + ikv_object_set_float(stats, "speed", 12.75); + ikv_object_set_bool(stats, "indoors", false); + return root; +} + static int test_object_metadata_and_scalars(test_context_t *context) { ikv_node_t *root = ikv_create_object("root"); @@ -925,7 +985,7 @@ static int test_hook_fopen_fail_parse_file(test_context_t *context) result = fail(context, "prep write failed"); else { - ikv_test_fail_fopen_after(3); + ikv_test_fail_fopen_after(2); result = expect_true(context, ikv_parse_file(path) == NULL, "fopen fail should break parse_file"); } cleanup_file(path); @@ -985,7 +1045,7 @@ static int test_hook_fread_fail_parse_file(test_context_t *context) result = fail(context, "prep write failed"); else { - ikv_test_fail_fread_after(3); + ikv_test_fail_fread_after(2); result = expect_true(context, ikv_parse_file(path) == NULL, "fread fail should break parse_file"); } cleanup_file(path); @@ -1164,6 +1224,92 @@ static int test_hook_alloc_fail_internal_v2_memory_copy(test_context_t *context) } #endif +static int test_binary_v2_file_benchmark(test_context_t *context) +{ + enum { benchmark_iterations = 100 }; + const char *path = "demo_benchmark_v2.ikvb"; + ikv_node_t *root = NULL; + double total_index_us = 0.0; + double total_read_us = 0.0; + double total_write_us = 0.0; + int result = 0; + + cleanup_file(path); + root = create_benchmark_root(); + if (!root) + return fail(context, "failed to create benchmark root"); + + if (!ikvb_write_file(path, root)) + result = fail(context, "failed to prepare benchmark file"); + + for (unsigned int i = 0; result == 0 && i < benchmark_iterations; ++i) + { + double start_time = benchmark_now_us(); + ikv_node_t *loaded = ikv_parse_file(path); + double end_time = benchmark_now_us(); + + if (!loaded) + result = fail(context, "benchmark index parse failed"); + else + total_index_us += end_time - start_time; + + ikv_free(loaded); + } + + for (unsigned int i = 0; result == 0 && i < benchmark_iterations; ++i) + { + ikv_node_t *loaded = ikv_parse_file(path); + ikv_node_t *inventory = NULL; + double start_time = 0.0; + double end_time = 0.0; + + if (!loaded) + { + result = fail(context, "benchmark read parse failed"); + break; + } + + start_time = benchmark_now_us(); + inventory = ikv_object_get(loaded, "inventory"); + end_time = benchmark_now_us(); + + if ((result = expect_true(context, inventory != NULL, "benchmark read inventory lookup failed")) == 0 && + (result = expect_true(context, ikv_array_size(inventory) == 5u, "benchmark read inventory size mismatch")) == 0) + { + total_read_us += end_time - start_time; + } + + ikv_free(loaded); + } + + for (unsigned int i = 0; result == 0 && i < benchmark_iterations; ++i) + { + double start_time = benchmark_now_us(); + bool ok = ikvb_write_file(path, root); + double end_time = benchmark_now_us(); + + if (!ok) + result = fail(context, "benchmark write failed"); + else + total_write_us += end_time - start_time; + } + + if (result == 0) + { + snprintf(context->message, + sizeof(context->message), + "avg index %.2fus avg read %.2fus avg write %.2fus over %u iterations", + total_index_us / (double)benchmark_iterations, + total_read_us / (double)benchmark_iterations, + total_write_us / (double)benchmark_iterations, + (unsigned int)benchmark_iterations); + } + + cleanup_file(path); + ikv_free(root); + return result; +} + static const test_case_t test_cases[] = { {"object metadata and scalars", test_object_metadata_and_scalars}, {"object replace and missing lookup", test_object_replace_and_missing_lookup}, @@ -1181,6 +1327,7 @@ static const test_case_t test_cases[] = { {"binary v1 file roundtrip", test_binary_v1_file_roundtrip}, {"binary v2 memory lazy roundtrip", test_binary_v2_memory_lazy_roundtrip}, {"binary v2 file lazy roundtrip", test_binary_v2_file_lazy_roundtrip}, + {"binary v2 file benchmark", test_binary_v2_file_benchmark}, {"detection apis", test_detection_apis}, {"detect binary version v2", test_detect_binary_version_v2}, {"explicit version apis", test_explicit_version_apis}, @@ -1252,7 +1399,7 @@ static void log_case(bool passed, unsigned int index, unsigned int total, long e total, name); - if (!passed && message && message[0] != 0) + if (message && message[0] != 0) printf(" %s- %s%s", ANSI_YELLOW, message, ANSI_RESET); putchar('\n'); diff --git a/src/ikv.c b/src/ikv.c index 56ea165..d3f16e7 100644 --- a/src/ikv.c +++ b/src/ikv.c @@ -203,6 +203,28 @@ static bool read_file_bytes(const char *path, uint8_t **out_data, size_t *out_si return true; } +static bool read_file_prefix(const char *path, uint8_t *prefix, size_t prefix_capacity, size_t *out_size) +{ + FILE *file = NULL; + size_t size = 0u; + + if (!path || !prefix || prefix_capacity == 0u || !out_size) + return false; + + *out_size = 0u; + prefix[0] = 0u; + + file = IKV_FOPEN(path, "rb"); + if (!file) + return false; + + size = IKV_FREAD(prefix, 1u, prefix_capacity - 1u, file); + IKV_FCLOSE(file); + prefix[size] = 0u; + *out_size = size; + return true; +} + static char *ikv_strdup(const char *s) { if (!s) @@ -2209,20 +2231,12 @@ ikv_version_t ikv_detect_binary_version(const void *data, size_t size) ikv_version_t ikv_detect_file_version(const char *path, bool binary) { uint8_t prefix[16] = {0}; - size_t size = 0; - FILE *file = NULL; + size_t size = 0u; ikv_version_t version = IKV_VERSION_UNKNOWN; - if (!path) + if (!read_file_prefix(path, prefix, sizeof(prefix), &size)) return IKV_VERSION_UNKNOWN; - file = IKV_FOPEN(path, "rb"); - if (!file) - return IKV_VERSION_UNKNOWN; - - size = IKV_FREAD(prefix, 1u, sizeof(prefix) - 1u, file); - IKV_FCLOSE(file); - version = binary ? ikv_detect_binary_version(prefix, size) : ikv_detect_text_version((const char *)prefix); return version; @@ -2265,13 +2279,18 @@ ikv_node_t *ikv_parse_string_version(const char *src, ikv_version_t version) ikv_node_t *ikv_parse_file(const char *path) { + uint8_t prefix[16] = {0}; + size_t prefix_size = 0u; ikv_version_t version = IKV_VERSION_UNKNOWN; - version = ikv_detect_file_version(path, true); + if (!read_file_prefix(path, prefix, sizeof(prefix), &prefix_size)) + return NULL; + + version = ikv_detect_binary_version(prefix, prefix_size); if (version != IKV_VERSION_UNKNOWN) return ikvb_parse_file_version(path, version); - version = ikv_detect_file_version(path, false); + version = ikv_detect_text_version((const char *)prefix); if (version == IKV_VERSION_UNKNOWN) version = IKV_VERSION_1; return ikv_parse_file_version(path, version); diff --git a/src/loaders/ikv2.c b/src/loaders/ikv2.c index cbe8977..91fd74f 100644 --- a/src/loaders/ikv2.c +++ b/src/loaders/ikv2.c @@ -30,7 +30,6 @@ typedef struct { ikv_lazy_state_t base; ikv2_source_kind_t source_kind; - char *file_path; FILE *file_handle; uint8_t *memory_data; size_t memory_size; @@ -146,23 +145,6 @@ static bool ikv2_buffer_reserve(ikv2_buffer_t *buffer, size_t additional) return true; } -static char *ikv2_strdup(const char *value) -{ - size_t length = 0u; - char *copy = NULL; - - if (!value) - value = ""; - - length = strlen(value); - copy = (char *)IKV_MALLOC(length + 1u); - if (!copy) - return NULL; - - memcpy(copy, value, length + 1u); - return copy; -} - static void ikv2_buffer_write_bytes(ikv2_buffer_t *buffer, const void *data, size_t size) { if (!ikv2_buffer_reserve(buffer, size)) @@ -495,7 +477,6 @@ static void ikv2_lazy_root_destroy(ikv_lazy_state_t *state) IKV_FCLOSE(lazy_root->file_handle); IKV_FREE(lazy_root->bucket_heads); IKV_FREE(lazy_root->entries); - IKV_FREE(lazy_root->file_path); IKV_FREE(lazy_root->memory_data); IKV_FREE(lazy_root); } @@ -856,10 +837,9 @@ static ikv_node_t *ikv2_parse_binary_file(const char *path) lazy_root->base.destroy = ikv2_lazy_root_destroy; lazy_root->base.load_object_key = ikv2_lazy_root_load_object_key; lazy_root->source_kind = IKV2_SOURCE_FILE; - lazy_root->file_path = ikv2_strdup(path); lazy_root->entry_count = entry_count; lazy_root->entries = entry_count ? (ikv2_index_entry_t *)IKV_CALLOC(entry_count, sizeof(*lazy_root->entries)) : NULL; - if (!lazy_root->file_path || (entry_count > 0u && !lazy_root->entries)) + if (entry_count > 0u && !lazy_root->entries) { ikv2_lazy_root_destroy(&lazy_root->base); ikv_free(root);