perf(io): reduce file path overhead and add benchmarks

This commit is contained in:
2026-06-17 10:44:20 -05:00
parent 7733cc90ff
commit eeed4c376d
3 changed files with 182 additions and 36 deletions

View File

@@ -5,6 +5,9 @@
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifdef _WIN32
#include <windows.h>
#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');

View File

@@ -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);

View File

@@ -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);