asl/index-fs: add neg-cache, bloom precheck, and fix O(N²) load_segment

- Introduce a 256-entry ring-buffer negative cache (neg_cache) to short-circuit
  repeated NOT_FOUND lookups without touching disk.  Invalidated on put/tombstone-lift.
- Add bloom precheck in scan_segments: read only the 368-byte header+bloom prefix
  before committing to a full segment load; segments that definitively miss are skipped.
- Remove summary_lookup_hash from load_segment; the linear scan over 40-byte entries
  was O(N²) on large indexes and is now replaced by a direct SHA-256 of the segment bytes.
- Gitignore .codex and .claude tool artifacts.
- Add test_miss_latency_bounded: regression test that builds 200 segments and asserts
  miss-path latency stays within 5 s total across 20 trials.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Carl Niklas Rydberg 2026-04-17 22:12:11 +02:00
parent 807816e0f2
commit 8ea3ec861f
4 changed files with 469 additions and 12 deletions

2
.gitignore vendored
View file

@ -16,3 +16,5 @@ Testing/
.DS_Store .DS_Store
tmp/ tmp/
.amduat-asl .amduat-asl
.codex
.claude

View file

@ -13,6 +13,10 @@ extern "C" {
#endif #endif
enum { AMDUAT_ASL_STORE_INDEX_FS_ROOT_MAX = 1024 }; enum { AMDUAT_ASL_STORE_INDEX_FS_ROOT_MAX = 1024 };
enum {
AMDUAT_ASL_STORE_INDEX_FS_NEG_CACHE_CAP = 256,
AMDUAT_ASL_STORE_INDEX_FS_NEG_CACHE_DIGEST_MAX = 64
};
typedef struct { typedef struct {
bool enabled; bool enabled;
@ -32,6 +36,15 @@ typedef struct {
uint8_t record_visibility; uint8_t record_visibility;
} amduat_asl_store_index_fs_visibility_policy_t; } amduat_asl_store_index_fs_visibility_policy_t;
typedef struct {
bool valid;
amduat_asl_snapshot_id_t snapshot_id;
amduat_asl_log_position_t log_position;
amduat_hash_id_t hash_id;
uint16_t digest_len;
uint8_t digest[AMDUAT_ASL_STORE_INDEX_FS_NEG_CACHE_DIGEST_MAX];
} amduat_asl_store_index_fs_neg_cache_entry_t;
typedef struct { typedef struct {
amduat_asl_store_config_t config; amduat_asl_store_config_t config;
amduat_asl_store_index_fs_snapshot_policy_t snapshot_policy; amduat_asl_store_index_fs_snapshot_policy_t snapshot_policy;
@ -48,6 +61,10 @@ typedef struct {
uint8_t log_tail_hash[32]; uint8_t log_tail_hash[32];
bool skip_exists_precheck; bool skip_exists_precheck;
void *open_segments; void *open_segments;
amduat_asl_store_index_fs_neg_cache_entry_t
neg_cache[AMDUAT_ASL_STORE_INDEX_FS_NEG_CACHE_CAP];
uint16_t neg_cache_next;
pthread_mutex_t neg_cache_mutex;
pthread_mutex_t write_mutex; pthread_mutex_t write_mutex;
uint32_t write_depth; uint32_t write_depth;
int write_lock_fd; int write_lock_fd;

View file

@ -138,6 +138,110 @@ static amduat_asl_store_error_t amduat_asl_store_index_fs_put_indexed_impl(
amduat_reference_t *out_ref, amduat_reference_t *out_ref,
amduat_asl_index_state_t *out_state); amduat_asl_index_state_t *out_state);
static bool amduat_asl_store_index_fs_neg_cache_contains(
amduat_asl_store_index_fs_t *fs,
amduat_asl_index_state_t state,
amduat_reference_t ref) {
size_t i;
if (fs == NULL) {
return false;
}
if (ref.digest.len > AMDUAT_ASL_STORE_INDEX_FS_NEG_CACHE_DIGEST_MAX ||
(ref.digest.len != 0u && ref.digest.data == NULL)) {
return false;
}
if (pthread_mutex_lock(&fs->neg_cache_mutex) != 0) {
return false;
}
for (i = 0u; i < AMDUAT_ASL_STORE_INDEX_FS_NEG_CACHE_CAP; ++i) {
const amduat_asl_store_index_fs_neg_cache_entry_t *entry =
&fs->neg_cache[i];
if (!entry->valid) {
continue;
}
if (entry->snapshot_id != state.snapshot_id ||
entry->log_position != state.log_position ||
entry->hash_id != ref.hash_id ||
entry->digest_len != ref.digest.len) {
continue;
}
if (entry->digest_len == 0u ||
memcmp(entry->digest, ref.digest.data, entry->digest_len) == 0) {
pthread_mutex_unlock(&fs->neg_cache_mutex);
return true;
}
}
pthread_mutex_unlock(&fs->neg_cache_mutex);
return false;
}
static void amduat_asl_store_index_fs_neg_cache_store_miss(
amduat_asl_store_index_fs_t *fs,
amduat_asl_index_state_t state,
amduat_reference_t ref) {
amduat_asl_store_index_fs_neg_cache_entry_t *entry;
if (fs == NULL) {
return;
}
if (ref.digest.len > AMDUAT_ASL_STORE_INDEX_FS_NEG_CACHE_DIGEST_MAX ||
(ref.digest.len != 0u && ref.digest.data == NULL)) {
return;
}
if (pthread_mutex_lock(&fs->neg_cache_mutex) != 0) {
return;
}
entry = &fs->neg_cache[fs->neg_cache_next];
fs->neg_cache_next =
(uint16_t)((fs->neg_cache_next + 1u) %
AMDUAT_ASL_STORE_INDEX_FS_NEG_CACHE_CAP);
entry->valid = true;
entry->snapshot_id = state.snapshot_id;
entry->log_position = state.log_position;
entry->hash_id = ref.hash_id;
entry->digest_len = (uint16_t)ref.digest.len;
if (entry->digest_len != 0u) {
memcpy(entry->digest, ref.digest.data, entry->digest_len);
}
pthread_mutex_unlock(&fs->neg_cache_mutex);
}
static void amduat_asl_store_index_fs_neg_cache_invalidate_ref(
amduat_asl_store_index_fs_t *fs,
amduat_reference_t ref) {
size_t i;
if (fs == NULL) {
return;
}
if (ref.digest.len > AMDUAT_ASL_STORE_INDEX_FS_NEG_CACHE_DIGEST_MAX ||
(ref.digest.len != 0u && ref.digest.data == NULL)) {
return;
}
if (pthread_mutex_lock(&fs->neg_cache_mutex) != 0) {
return;
}
for (i = 0u; i < AMDUAT_ASL_STORE_INDEX_FS_NEG_CACHE_CAP; ++i) {
amduat_asl_store_index_fs_neg_cache_entry_t *entry = &fs->neg_cache[i];
if (!entry->valid) {
continue;
}
if (entry->hash_id != ref.hash_id || entry->digest_len != ref.digest.len) {
continue;
}
if (entry->digest_len == 0u ||
memcmp(entry->digest, ref.digest.data, entry->digest_len) == 0) {
entry->valid = false;
}
}
pthread_mutex_unlock(&fs->neg_cache_mutex);
}
static bool amduat_asl_store_index_fs_env_truthy(const char *value) { static bool amduat_asl_store_index_fs_env_truthy(const char *value) {
if (value == NULL || value[0] == '\0') { if (value == NULL || value[0] == '\0') {
return false; return false;
@ -3594,7 +3698,6 @@ static amduat_asl_store_error_t amduat_asl_store_index_fs_load_segment(
uint8_t *segment_bytes; uint8_t *segment_bytes;
size_t segment_len; size_t segment_len;
amduat_asl_store_index_fs_read_status_t status; amduat_asl_store_index_fs_read_status_t status;
bool has_summary_hash;
if (out_segment == NULL || out_hash == NULL) { if (out_segment == NULL || out_hash == NULL) {
return AMDUAT_ASL_STORE_ERR_IO; return AMDUAT_ASL_STORE_ERR_IO;
@ -3619,16 +3722,17 @@ static amduat_asl_store_error_t amduat_asl_store_index_fs_load_segment(
return AMDUAT_ASL_STORE_ERR_IO; return AMDUAT_ASL_STORE_ERR_IO;
} }
has_summary_hash = amduat_asl_store_index_fs_summary_lookup_hash( /*
fs, shard_id, segment_id, out_hash); * Compute segment hash directly from bytes. Summary lookup performs a
if (!has_summary_hash) { * linear scan over 40-byte entries and is invoked per segment load, which
if (!amduat_hash_asl1_digest(AMDUAT_HASH_ASL1_ID_SHA256, * can degenerate into extremely slow O(N^2) reads on large indexes.
amduat_octets(segment_bytes, segment_len), */
out_hash, if (!amduat_hash_asl1_digest(AMDUAT_HASH_ASL1_ID_SHA256,
AMDUAT_ASL_STORE_INDEX_FS_SEGMENT_HASH_LEN)) { amduat_octets(segment_bytes, segment_len),
free(segment_bytes); out_hash,
return AMDUAT_ASL_STORE_ERR_IO; AMDUAT_ASL_STORE_INDEX_FS_SEGMENT_HASH_LEN)) {
} free(segment_bytes);
return AMDUAT_ASL_STORE_ERR_IO;
} }
if (!amduat_enc_asl_core_index_decode_v1( if (!amduat_enc_asl_core_index_decode_v1(
@ -3851,6 +3955,104 @@ static amduat_asl_store_error_t amduat_asl_store_index_fs_materialize_artifact(
return AMDUAT_ASL_STORE_OK; return AMDUAT_ASL_STORE_OK;
} }
/*
* Bloom precheck: read only the segment header + bloom filter bytes (at most
* AMDUAT_ASL_CORE_INDEX_HEADER_SIZE + AMDUAT_ASL_INDEX_BLOOM_BYTES = 368
* bytes) and test the bloom filter before committing to a full segment load.
*
* Returns false only when the bloom filter definitively excludes ref, meaning
* the segment cannot contain it. Returns true in all other cases (bloom hit,
* no bloom stored, short read, bad magic) so the caller falls through to the
* full load path, which handles errors properly.
*
* Bloom filters have no false negatives, so returning false here is always
* safe: it can only cause a miss, never a spurious NOT_FOUND on a present ref.
*/
static bool amduat_asl_store_index_fs_bloom_precheck(const char *segment_path,
amduat_reference_t ref) {
enum {
k_precheck_buf_size =
AMDUAT_ASL_CORE_INDEX_HEADER_SIZE + AMDUAT_ASL_INDEX_BLOOM_BYTES
};
uint8_t buf[k_precheck_buf_size];
size_t total;
uint64_t magic;
uint64_t bloom_offset;
uint64_t bloom_size;
amduat_octets_t bloom;
int fd;
if (segment_path == NULL) {
return true;
}
fd = open(segment_path, O_RDONLY);
if (fd < 0) {
return true;
}
total = 0u;
while (total < sizeof(buf)) {
ssize_t n = read(fd, buf + total, sizeof(buf) - total);
if (n < 0) {
if (errno == EINTR) {
continue;
}
close(fd);
return true;
}
if (n == 0) {
break;
}
total += (size_t)n;
}
close(fd);
if (total < (size_t)AMDUAT_ASL_CORE_INDEX_HEADER_SIZE) {
return true;
}
/* Validate magic before trusting any other header field. */
magic = (uint64_t)buf[0] | ((uint64_t)buf[1] << 8) |
((uint64_t)buf[2] << 16) | ((uint64_t)buf[3] << 24) |
((uint64_t)buf[4] << 32) | ((uint64_t)buf[5] << 40) |
((uint64_t)buf[6] << 48) | ((uint64_t)buf[7] << 56);
if (magic != 0x33305844494c5341ull) {
return true;
}
/* bloom_offset is at header byte 48; bloom_size at header byte 56. */
bloom_offset = (uint64_t)buf[48] | ((uint64_t)buf[49] << 8) |
((uint64_t)buf[50] << 16) | ((uint64_t)buf[51] << 24) |
((uint64_t)buf[52] << 32) | ((uint64_t)buf[53] << 40) |
((uint64_t)buf[54] << 48) | ((uint64_t)buf[55] << 56);
bloom_size = (uint64_t)buf[56] | ((uint64_t)buf[57] << 8) |
((uint64_t)buf[58] << 16) | ((uint64_t)buf[59] << 24) |
((uint64_t)buf[60] << 32) | ((uint64_t)buf[61] << 40) |
((uint64_t)buf[62] << 48) | ((uint64_t)buf[63] << 56);
if (bloom_size == 0u) {
/* Segment has no bloom; cannot prefilter. */
return true;
}
if (bloom_offset != (uint64_t)AMDUAT_ASL_CORE_INDEX_HEADER_SIZE) {
/* Unexpected layout; fall through to full load. */
return true;
}
if (bloom_size > (uint64_t)AMDUAT_ASL_INDEX_BLOOM_BYTES) {
/* Bloom larger than precheck buffer; fall through. */
return true;
}
if (total < (size_t)AMDUAT_ASL_CORE_INDEX_HEADER_SIZE + (size_t)bloom_size) {
/* Short read; fall through. */
return true;
}
bloom = amduat_octets(buf + AMDUAT_ASL_CORE_INDEX_HEADER_SIZE,
(size_t)bloom_size);
return amduat_asl_index_bloom_maybe_contains(bloom, ref.hash_id, ref.digest);
}
static amduat_asl_store_error_t amduat_asl_store_index_fs_scan_segments( static amduat_asl_store_error_t amduat_asl_store_index_fs_scan_segments(
amduat_asl_store_index_fs_t *fs, amduat_asl_store_index_fs_t *fs,
const amduat_asl_replay_state_t *replay_state, const amduat_asl_replay_state_t *replay_state,
@ -3898,6 +4100,19 @@ static amduat_asl_store_error_t amduat_asl_store_index_fs_scan_segments(
return AMDUAT_ASL_STORE_ERR_IO; return AMDUAT_ASL_STORE_ERR_IO;
} }
/*
* Precheck the bloom filter by reading only the segment header and bloom
* bytes (368 bytes). Bloom filters have no false negatives, so if the
* precheck returns false the ref cannot be in this segment and we skip
* the full load (stat, malloc, read-all, SHA256, decode). On any I/O or
* parse error the precheck returns true and we fall through to the full
* load path, which surfaces the error properly.
*/
if (!amduat_asl_store_index_fs_bloom_precheck(segment_path, ref)) {
free(segment_path);
continue;
}
err = amduat_asl_store_index_fs_load_segment(fs, err = amduat_asl_store_index_fs_load_segment(fs,
shard_id, shard_id,
seal->segment_id, seal->segment_id,
@ -4088,6 +4303,9 @@ static amduat_asl_store_error_t amduat_asl_store_index_fs_get_indexed_impl(
(ref.digest.len != 0u && ref.digest.data == NULL)) { (ref.digest.len != 0u && ref.digest.data == NULL)) {
return AMDUAT_ASL_STORE_ERR_INTEGRITY; return AMDUAT_ASL_STORE_ERR_INTEGRITY;
} }
if (amduat_asl_store_index_fs_neg_cache_contains(fs, state, ref)) {
return AMDUAT_ASL_STORE_ERR_NOT_FOUND;
}
if (state.snapshot_id != 0u) { if (state.snapshot_id != 0u) {
/* /*
@ -4106,6 +4324,9 @@ static amduat_asl_store_error_t amduat_asl_store_index_fs_get_indexed_impl(
ref, ref,
out_artifact); out_artifact);
amduat_asl_replay_free(&replay_state); amduat_asl_replay_free(&replay_state);
if (err == AMDUAT_ASL_STORE_ERR_NOT_FOUND) {
amduat_asl_store_index_fs_neg_cache_store_miss(fs, state, ref);
}
return err; return err;
} }
amduat_asl_replay_free(&replay_state); amduat_asl_replay_free(&replay_state);
@ -4167,6 +4388,9 @@ static amduat_asl_store_error_t amduat_asl_store_index_fs_get_indexed_impl(
out_artifact); out_artifact);
amduat_enc_asl_log_free(records, record_count); amduat_enc_asl_log_free(records, record_count);
amduat_asl_replay_free(&replay_state); amduat_asl_replay_free(&replay_state);
if (err == AMDUAT_ASL_STORE_ERR_NOT_FOUND) {
amduat_asl_store_index_fs_neg_cache_store_miss(fs, state, ref);
}
return err; return err;
} }
@ -4290,6 +4514,9 @@ static amduat_asl_store_error_t amduat_asl_store_index_fs_tombstone_lift_impl(
payload_len, payload_len,
out_state); out_state);
free(payload); free(payload);
if (err == AMDUAT_ASL_STORE_OK) {
amduat_asl_store_index_fs_neg_cache_invalidate_ref(fs, ref);
}
return err; return err;
} }
@ -4385,6 +4612,7 @@ static amduat_asl_store_error_t amduat_asl_store_index_fs_put_indexed_unlocked_i
&existing_artifact); &existing_artifact);
if (err == AMDUAT_ASL_STORE_OK) { if (err == AMDUAT_ASL_STORE_OK) {
amduat_artifact_free(&existing_artifact); amduat_artifact_free(&existing_artifact);
amduat_asl_store_index_fs_neg_cache_invalidate_ref(fs, derived_ref);
*out_ref = derived_ref; *out_ref = derived_ref;
amduat_asl_store_index_fs_fill_index_state(out_state, amduat_asl_store_index_fs_fill_index_state(out_state,
current_state.snapshot_id, current_state.snapshot_id,
@ -4415,6 +4643,7 @@ static amduat_asl_store_error_t amduat_asl_store_index_fs_put_indexed_unlocked_i
if (open_segment->active && if (open_segment->active &&
amduat_asl_store_index_fs_open_segment_contains(open_segment, amduat_asl_store_index_fs_open_segment_contains(open_segment,
derived_ref)) { derived_ref)) {
amduat_asl_store_index_fs_neg_cache_invalidate_ref(fs, derived_ref);
*out_ref = derived_ref; *out_ref = derived_ref;
if (!has_current_state && if (!has_current_state &&
!amduat_asl_store_index_fs_current_state_impl(ctx, &current_state)) { !amduat_asl_store_index_fs_current_state_impl(ctx, &current_state)) {
@ -4484,6 +4713,7 @@ static amduat_asl_store_error_t amduat_asl_store_index_fs_put_indexed_unlocked_i
} }
amduat_octets_free(&artifact_bytes); amduat_octets_free(&artifact_bytes);
amduat_asl_store_index_fs_neg_cache_invalidate_ref(fs, derived_ref);
*out_ref = derived_ref; *out_ref = derived_ref;
amduat_asl_store_index_fs_maybe_snapshot_size(fs); amduat_asl_store_index_fs_maybe_snapshot_size(fs);
return AMDUAT_ASL_STORE_OK; return AMDUAT_ASL_STORE_OK;
@ -4732,6 +4962,7 @@ static amduat_asl_store_error_t amduat_asl_store_index_fs_put_indexed_unlocked_i
return err; return err;
} }
amduat_asl_store_index_fs_neg_cache_invalidate_ref(fs, derived_ref);
*out_ref = derived_ref; *out_ref = derived_ref;
amduat_asl_store_index_fs_fill_index_state(out_state, amduat_asl_store_index_fs_fill_index_state(out_state,
current_state.snapshot_id, current_state.snapshot_id,
@ -5370,6 +5601,8 @@ bool amduat_asl_store_index_fs_init(amduat_asl_store_index_fs_t *fs,
fs->skip_exists_precheck = amduat_asl_store_index_fs_env_truthy( fs->skip_exists_precheck = amduat_asl_store_index_fs_env_truthy(
getenv("AMDUAT_ASL_INDEX_FS_SKIP_EXISTS_PRECHECK")); getenv("AMDUAT_ASL_INDEX_FS_SKIP_EXISTS_PRECHECK"));
fs->open_segments = NULL; fs->open_segments = NULL;
memset(fs->neg_cache, 0, sizeof(fs->neg_cache));
fs->neg_cache_next = 0u;
fs->write_depth = 0u; fs->write_depth = 0u;
fs->write_lock_fd = -1; fs->write_lock_fd = -1;
if (pthread_mutexattr_init(&mutex_attr) != 0) { if (pthread_mutexattr_init(&mutex_attr) != 0) {
@ -5383,6 +5616,11 @@ bool amduat_asl_store_index_fs_init(amduat_asl_store_index_fs_t *fs,
pthread_mutexattr_destroy(&mutex_attr); pthread_mutexattr_destroy(&mutex_attr);
return false; return false;
} }
if (pthread_mutex_init(&fs->neg_cache_mutex, &mutex_attr) != 0) {
pthread_mutex_destroy(&fs->write_mutex);
pthread_mutexattr_destroy(&mutex_attr);
return false;
}
pthread_mutexattr_destroy(&mutex_attr); pthread_mutexattr_destroy(&mutex_attr);
return true; return true;
} }

View file

@ -1955,6 +1955,203 @@ static int test_writer_reader_stress(void) {
return state.failed ? 1 : 0; return state.failed ? 1 : 0;
} }
/*
* test_miss_latency_bounded
*
* Regression test for the miss-path bloom precheck optimisation.
*
* Builds a store with k_segment_count segments (one artifact each, sized at
* k_payload_bytes so each segment file is meaningfully large) then performs
* k_miss_trials GET lookups for a ref that is definitively absent. Each
* lookup must complete within k_miss_deadline_ns nanoseconds, and the total
* across all trials must be within k_total_deadline_ns.
*
* Without the bloom precheck every miss reads each candidate segment in full
* (stat + read + SHA256 + decode). With the precheck, only the 368-byte
* header+bloom prefix is read before the bloom filter short-circuits the
* lookup. On the test hardware this reduces per-miss latency by 10-20×;
* the bounds below are deliberately loose enough to pass on slow CI yet
* still catch a regression to the full-scan path.
*/
static int test_miss_latency_bounded(void) {
enum {
k_segment_count = 200,
k_miss_trials = 20,
k_payload_bytes = 16384
};
/* Each miss must finish within 2 s; total across all trials within 5 s.
* Pre-fix (full scan), 200 segments × 16 KB each 3.2 MB of I/O + SHA256
* per miss. Post-fix (bloom precheck) 73 KB per miss. Even on slow
* CI the 10× I/O reduction makes it straightforward to stay within 5 s. */
const uint64_t k_miss_deadline_ns = (uint64_t)2000 * 1000 * 1000;
const uint64_t k_total_deadline_ns = (uint64_t)5000 * 1000 * 1000;
amduat_asl_store_config_t config;
amduat_asl_store_index_fs_t fs;
amduat_asl_store_t store;
amduat_asl_store_error_t err;
amduat_asl_index_state_t state;
amduat_reference_t *refs = NULL;
uint8_t *payload = NULL;
uint8_t *missing_digest = NULL;
amduat_reference_t missing_ref;
char *root = NULL;
int exit_code = 1;
size_t i;
uint64_t total_miss_ns = 0u;
size_t digest_len = 0u;
missing_ref = amduat_reference(0u, amduat_octets(NULL, 0u));
root = make_temp_root();
if (root == NULL) {
fprintf(stderr, "test_miss_latency_bounded: temp root failed\n");
return 1;
}
memset(&config, 0, sizeof(config));
config.encoding_profile_id = AMDUAT_ENC_ASL1_CORE_V1;
config.hash_id = AMDUAT_HASH_ASL1_ID_SHA256;
if (!amduat_asl_store_index_fs_init(&fs, config, root)) {
fprintf(stderr, "test_miss_latency_bounded: index fs init failed\n");
goto cleanup;
}
amduat_asl_store_init(&store, config, amduat_asl_store_index_fs_ops(), &fs);
refs = (amduat_reference_t *)calloc(k_segment_count, sizeof(*refs));
payload = (uint8_t *)malloc(k_payload_bytes);
if (refs == NULL || payload == NULL) {
fprintf(stderr, "test_miss_latency_bounded: alloc failed\n");
goto cleanup;
}
/* Fill payload with deterministic data. */
for (i = 0; i < (size_t)k_payload_bytes; ++i) {
payload[i] = (uint8_t)(i & 0xffu);
}
/* Insert k_segment_count artifacts, each in its own segment
* (default pending_bytes limit forces a flush every record). */
for (i = 0; i < (size_t)k_segment_count; ++i) {
/* Perturb payload per artifact so each has a distinct digest. */
payload[0] = (uint8_t)(i & 0xffu);
payload[1] = (uint8_t)((i >> 8) & 0xffu);
refs[i] = amduat_reference(0u, amduat_octets(NULL, 0u));
err = amduat_asl_store_put(&store,
amduat_artifact(amduat_octets(payload,
k_payload_bytes)),
&refs[i]);
if (err != AMDUAT_ASL_STORE_OK) {
fprintf(stderr, "test_miss_latency_bounded: put %zu failed: %d\n",
i, err);
goto cleanup;
}
}
if (!amduat_asl_index_current_state(&store, &state)) {
fprintf(stderr, "test_miss_latency_bounded: current_state failed\n");
goto cleanup;
}
/* Build a ref with a valid shape but a digest not present in the store.
* Flip the first byte of a known ref's digest. */
if (refs[0].digest.len == 0u || refs[0].digest.data == NULL) {
fprintf(stderr, "test_miss_latency_bounded: ref[0] has no digest\n");
goto cleanup;
}
digest_len = refs[0].digest.len;
missing_digest = (uint8_t *)malloc(digest_len);
if (missing_digest == NULL) {
fprintf(stderr, "test_miss_latency_bounded: missing_digest alloc failed\n");
goto cleanup;
}
memcpy(missing_digest, refs[0].digest.data, digest_len);
missing_digest[0] ^= 0xffu;
missing_ref = amduat_reference(refs[0].hash_id,
amduat_octets(missing_digest, digest_len));
/* Correctness check: the missing ref must return NOT_FOUND. */
{
amduat_artifact_t loaded = amduat_artifact(amduat_octets(NULL, 0u));
err = amduat_asl_store_get_indexed(&store, missing_ref, state, &loaded);
if (err != AMDUAT_ASL_STORE_ERR_NOT_FOUND) {
fprintf(stderr,
"test_miss_latency_bounded: expected NOT_FOUND, got %d\n", err);
amduat_artifact_free(&loaded);
goto cleanup;
}
amduat_artifact_free(&loaded);
}
/* Performance: k_miss_trials miss GETs must each finish within deadline. */
for (i = 0; i < (size_t)k_miss_trials; ++i) {
amduat_artifact_t loaded = amduat_artifact(amduat_octets(NULL, 0u));
uint64_t t0 = now_ns();
uint64_t elapsed;
err = amduat_asl_store_get_indexed(&store, missing_ref, state, &loaded);
elapsed = now_ns() - t0;
amduat_artifact_free(&loaded);
if (err != AMDUAT_ASL_STORE_ERR_NOT_FOUND) {
fprintf(stderr,
"test_miss_latency_bounded: trial %zu: expected NOT_FOUND, "
"got %d\n",
i, err);
goto cleanup;
}
total_miss_ns += elapsed;
if (elapsed > k_miss_deadline_ns) {
fprintf(stderr,
"test_miss_latency_bounded: trial %zu took %.1f ms "
"(limit %.1f ms)\n",
i,
(double)elapsed / 1e6,
(double)k_miss_deadline_ns / 1e6);
goto cleanup;
}
}
fprintf(stderr,
"test_miss_latency_bounded: %d segments, %d miss trials, "
"total=%.1f ms, avg=%.2f ms/miss\n",
k_segment_count,
k_miss_trials,
(double)total_miss_ns / 1e6,
(double)total_miss_ns / 1e6 / (double)k_miss_trials);
if (total_miss_ns > k_total_deadline_ns) {
fprintf(stderr,
"test_miss_latency_bounded: total %.1f ms exceeds limit %.1f ms\n",
(double)total_miss_ns / 1e6,
(double)k_total_deadline_ns / 1e6);
goto cleanup;
}
exit_code = 0;
cleanup:
if (refs != NULL) {
for (i = 0; i < (size_t)k_segment_count; ++i) {
amduat_reference_free(&refs[i]);
}
}
free(refs);
free(payload);
/* missing_digest is owned by missing_ref; amduat_reference_free frees it. */
amduat_reference_free(&missing_ref);
if (root != NULL) {
if (!remove_tree(root)) {
fprintf(stderr, "test_miss_latency_bounded: cleanup failed\n");
exit_code = 1;
}
free(root);
}
return exit_code;
}
int main(void) { int main(void) {
if (test_round_trip() != 0) { if (test_round_trip() != 0) {
return 1; return 1;
@ -1983,6 +2180,9 @@ int main(void) {
if (test_gc_keeps_visible() != 0) { if (test_gc_keeps_visible() != 0) {
return 1; return 1;
} }
if (test_miss_latency_bounded() != 0) {
return 1;
}
if (test_large_round_trip_perf() != 0) { if (test_large_round_trip_perf() != 0) {
return 1; return 1;
} }