perf(io): reduce file path overhead and add benchmarks
This commit is contained in:
153
demo/unit_test.c
153
demo/unit_test.c
@@ -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');
|
||||
|
||||
43
src/ikv.c
43
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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user