| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| /* |
| * agehash.c - Robin Hood open-addressing hashtable for AGE. |
| * |
| * See agehash.h for the public contract. This file implements the INLINE |
| * mode only. |
| * |
| * Internal slot layout (INLINE): |
| * |
| * bytes 0..1 uint16 probe_dist (AGEHASH_EMPTY = 0xFFFF marks empty) |
| * bytes 2..3 uint16 reserved (future tombstone / flag bits) |
| * bytes 4..7 uint32 pad (forces key to 8-byte alignment) |
| * bytes 8..K+7 key |
| * bytes K+8.. payload |
| * |
| * slot_size = MAXALIGN(8 + key_size + payload_size). |
| */ |
| |
| #include "postgres.h" |
| |
| #include "fmgr.h" |
| #include "utils/agehash.h" |
| #include "utils/builtins.h" |
| #include "utils/memutils.h" |
| |
| /* ------------------------------------------------------------------------- */ |
| |
| struct AgeHashTable |
| { |
| /* Slot array: capacity * slot_size bytes, palloc'd in mcxt. */ |
| char *slots; |
| uint32 capacity; /* always a power of two */ |
| uint32 capacity_mask; /* capacity - 1 */ |
| uint32 size; /* live entries */ |
| uint32 max_size; /* size at which we grow */ |
| uint32 slot_size; /* total bytes per slot */ |
| uint32 key_size; |
| uint32 payload_size; |
| uint32 payload_offset; /* AGEHASH_SLOT_KEY_OFFSET + key_size */ |
| AgeHashMode mode; |
| bool frozen; |
| agehash_hash_fn hash_fn; |
| agehash_keyeq_fn keyeq_fn; |
| MemoryContext mcxt; |
| }; |
| |
| /* ------------------------------------------------------------------------- */ |
| /* Slot accessors. */ |
| |
| static inline char * |
| slot_at(AgeHashTable *t, uint32 idx) |
| { |
| return t->slots + (Size) idx * t->slot_size; |
| } |
| |
| static inline uint16 |
| slot_probe_dist(const char *slot) |
| { |
| uint16 d; |
| memcpy(&d, slot, sizeof(uint16)); |
| return d; |
| } |
| |
| static inline void |
| slot_set_probe_dist(char *slot, uint16 d) |
| { |
| memcpy(slot, &d, sizeof(uint16)); |
| } |
| |
| static inline char * |
| slot_key_ptr(AgeHashTable *t, char *slot) |
| { |
| (void) t; |
| return slot + AGEHASH_SLOT_KEY_OFFSET; |
| } |
| |
| static inline char * |
| slot_payload_ptr(AgeHashTable *t, char *slot) |
| { |
| return slot + t->payload_offset; |
| } |
| |
| /* ------------------------------------------------------------------------- */ |
| /* Construction. */ |
| |
| static uint32 |
| next_pow2(uint32 v) |
| { |
| uint32 p = 1; |
| while (p < v) |
| p <<= 1; |
| return p; |
| } |
| |
| AgeHashTable * |
| agehash_create_inline(MemoryContext mcxt, |
| Size key_size, |
| Size payload_size, |
| uint32 capacity_hint, |
| agehash_hash_fn hash_fn, |
| agehash_keyeq_fn keyeq_fn) |
| { |
| AgeHashTable *t; |
| MemoryContext oldctx; |
| uint32 min_cap; |
| uint32 cap; |
| |
| Assert(mcxt != NULL); |
| Assert(key_size > 0 && key_size <= 64); |
| Assert(payload_size > 0 && payload_size <= 4096); |
| Assert(hash_fn != NULL); |
| Assert(keyeq_fn != NULL); |
| |
| /* |
| * Runtime enforcement of the on-stack carrier limits used by the inline |
| * Robin Hood path (carry_key[64], carry_payload[4096]). Asserts above |
| * give early diagnostics in debug builds; this elog covers production |
| * builds where Asserts compile out and the same caller would otherwise |
| * trigger a stack-buffer overflow during insert. |
| */ |
| if (key_size == 0 || key_size > 64) |
| { |
| ereport(ERROR, |
| (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), |
| errmsg("agehash inline key size %zu out of range (must be 1..64)", |
| (size_t) key_size))); |
| } |
| if (payload_size == 0 || payload_size > 4096) |
| { |
| ereport(ERROR, |
| (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), |
| errmsg("agehash inline payload size %zu out of range (must be 1..4096)", |
| (size_t) payload_size))); |
| } |
| |
| oldctx = MemoryContextSwitchTo(mcxt); |
| |
| t = palloc0(sizeof(AgeHashTable)); |
| t->mcxt = mcxt; |
| t->mode = AGEHASH_INLINE; |
| t->frozen = false; |
| t->hash_fn = hash_fn; |
| t->keyeq_fn = keyeq_fn; |
| t->key_size = (uint32) key_size; |
| t->payload_size = (uint32) payload_size; |
| /* |
| * MAXALIGN payload_offset so that the typed payload pointer returned |
| * by slot_payload_ptr() is suitably aligned for any C type the caller |
| * might cast to. Without this, a key_size that is not a multiple of |
| * MAXIMUM_ALIGNOF (e.g. a 12-byte key) would leave the payload at a |
| * misaligned address — undefined behavior under strict alignment rules |
| * even though it works in practice on x86_64. |
| */ |
| t->payload_offset = MAXALIGN(AGEHASH_SLOT_KEY_OFFSET + (uint32) key_size); |
| t->slot_size = MAXALIGN(t->payload_offset + (uint32) payload_size); |
| /* |
| * agehash_key_from_payload() recovers the key as (payload - key_size), |
| * which is only valid when the payload abuts the key with no MAXALIGN |
| * padding between them. That holds iff key_size is a multiple of |
| * MAXIMUM_ALIGNOF. Enforce the invariant here so a future non-aligned |
| * key trips in DEBUG builds rather than silently handing the macro a |
| * wrong pointer. |
| */ |
| if (key_size % MAXIMUM_ALIGNOF != 0) |
| { |
| ereport(ERROR, |
| (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), |
| errmsg("agehash inline key size %zu must be a multiple of %d for key recovery", |
| (size_t) key_size, MAXIMUM_ALIGNOF))); |
| } |
| Assert(t->payload_offset == AGEHASH_SLOT_KEY_OFFSET + (uint32) key_size); |
| |
| /* |
| * Capacity floor of 64 keeps tiny tables out of degenerate-load territory |
| * and avoids a flurry of grows on the first few inserts. |
| */ |
| if (capacity_hint == 0) |
| min_cap = 64; |
| else |
| { |
| /* size capacity_hint at MAX_LOAD so we don't immediately grow */ |
| min_cap = (uint32) ((double) capacity_hint / AGEHASH_MAX_LOAD) + 1; |
| if (min_cap < 64) |
| min_cap = 64; |
| } |
| cap = next_pow2(min_cap); |
| Assert((cap & (cap - 1)) == 0); |
| |
| t->capacity = cap; |
| t->capacity_mask = cap - 1; |
| t->size = 0; |
| t->max_size = (uint32) ((double) cap * AGEHASH_MAX_LOAD); |
| /* |
| * The slot array can comfortably exceed 1 GiB on production graphs |
| * (the SF3 ldbc_snb edge_table is multiple GiB at the 0.85 load |
| * factor). Use the HUGE allocator to bypass the standard MaxAllocSize |
| * check. |
| */ |
| t->slots = (char *) MemoryContextAllocHuge(mcxt, |
| (Size) cap * t->slot_size); |
| |
| /* Mark every slot empty. */ |
| { |
| uint32 i; |
| for (i = 0; i < cap; i++) |
| slot_set_probe_dist(slot_at(t, i), AGEHASH_EMPTY); |
| } |
| |
| MemoryContextSwitchTo(oldctx); |
| return t; |
| } |
| |
| /* ------------------------------------------------------------------------- */ |
| /* Insert. Robin Hood with rich-poor swap. */ |
| |
| static void agehash_grow(AgeHashTable *t); |
| |
| static void * |
| agehash_insert_internal(AgeHashTable *t, const void *key, uint32 hashvalue, |
| bool *found) |
| { |
| uint32 i; |
| uint16 d; |
| /* |
| * Carrier for the entry currently being placed. Starts as the caller's |
| * key with a fresh, zero-filled payload; gets overwritten on each |
| * Robin Hood swap. |
| */ |
| char carry_key[64]; |
| char carry_payload[4096]; |
| void *result_payload = NULL; |
| bool placed_caller = false; |
| |
| /* |
| * agehash_freeze() is documented to make subsequent inserts/grows fail |
| * loudly. The Assert below catches violations in debug builds; the |
| * runtime check guarantees the contract in production builds where |
| * Asserts compile out, preventing silent slot reordering that would |
| * invalidate any payload pointers callers consider stable post-freeze. |
| */ |
| if (t->frozen) |
| { |
| elog(ERROR, "agehash: insert into frozen table"); |
| } |
| Assert(!t->frozen); |
| Assert(t->key_size <= sizeof(carry_key)); |
| Assert(t->payload_size <= sizeof(carry_payload)); |
| |
| /* Grow before insert if at threshold. */ |
| if (t->size >= t->max_size) |
| agehash_grow(t); |
| |
| /* Initialize carry buffers with the caller's key and an empty payload. */ |
| memcpy(carry_key, key, t->key_size); |
| memset(carry_payload, 0, t->payload_size); |
| |
| i = hashvalue & t->capacity_mask; |
| d = 0; |
| |
| for (;;) |
| { |
| char *slot = slot_at(t, i); |
| uint16 sd = slot_probe_dist(slot); |
| |
| if (sd == AGEHASH_EMPTY) |
| { |
| /* Place the carrier here and we're done. */ |
| slot_set_probe_dist(slot, d); |
| memcpy(slot_key_ptr(t, slot), carry_key, t->key_size); |
| memcpy(slot_payload_ptr(t, slot), carry_payload, t->payload_size); |
| t->size++; |
| if (!placed_caller) |
| { |
| /* The caller's slot landed here. */ |
| if (found != NULL) |
| *found = false; |
| return slot_payload_ptr(t, slot); |
| } |
| /* |
| * The caller was placed earlier via a swap; result_payload |
| * already points at their final slot. |
| */ |
| Assert(result_payload != NULL); |
| return result_payload; |
| } |
| |
| if (sd == d && |
| !placed_caller && |
| t->keyeq_fn(slot_key_ptr(t, slot), carry_key, t->key_size)) |
| { |
| /* |
| * Existing entry with the caller's key. Note: this match check |
| * is only relevant before we've performed a swap; once we've |
| * placed the caller into a slot, the key in `carry` is some |
| * displaced entry that, by RH invariant on insert from a fresh |
| * key, cannot already exist in the table. |
| */ |
| if (found != NULL) |
| *found = true; |
| return slot_payload_ptr(t, slot); |
| } |
| |
| if (sd < d) |
| { |
| /* |
| * Rich-poor swap: this slot's owner is closer to its ideal |
| * bucket than we are. Take its place and continue with the |
| * displaced entry. If we have not yet placed the caller, this |
| * is where they end up; remember the pointer so we can return |
| * it once the displaced chain finishes. |
| */ |
| char tmp_key[64]; |
| char tmp_payload[4096]; |
| uint16 tmp_d = sd; |
| |
| memcpy(tmp_key, slot_key_ptr(t, slot), t->key_size); |
| memcpy(tmp_payload, slot_payload_ptr(t, slot), t->payload_size); |
| |
| slot_set_probe_dist(slot, d); |
| memcpy(slot_key_ptr(t, slot), carry_key, t->key_size); |
| memcpy(slot_payload_ptr(t, slot), carry_payload, t->payload_size); |
| |
| if (!placed_caller) |
| { |
| placed_caller = true; |
| result_payload = slot_payload_ptr(t, slot); |
| /* Notify caller: this insert is a fresh entry. */ |
| if (found != NULL) |
| { |
| *found = false; |
| found = NULL; /* don't write again */ |
| } |
| } |
| |
| /* Continue with the displaced entry as the new carrier. */ |
| memcpy(carry_key, tmp_key, t->key_size); |
| memcpy(carry_payload, tmp_payload, t->payload_size); |
| d = tmp_d; |
| } |
| |
| i = (i + 1) & t->capacity_mask; |
| d++; |
| |
| /* |
| * Probe distance overflow guard. With AGEHASH_MAX_LOAD = 0.85 and a |
| * non-degenerate hash function, max probe is expected to remain far |
| * below this limit in practice. The 0xFE00 ceiling reserves |
| * headroom while leaving probe_dist well clear of the AGEHASH_EMPTY |
| * sentinel. |
| */ |
| Assert(d < 0xFE00); |
| if (unlikely(d >= 0xFE00)) |
| elog(ERROR, "agehash: probe distance overflow (likely a bad hash function)"); |
| } |
| } |
| |
| void * |
| agehash_insert(AgeHashTable *t, const void *key, bool *found) |
| { |
| uint32 h = t->hash_fn(key, t->key_size); |
| return agehash_insert_internal(t, key, h, found); |
| } |
| |
| void * |
| agehash_insert_with_hash(AgeHashTable *t, const void *key, |
| uint32 hashvalue, bool *found) |
| { |
| return agehash_insert_internal(t, key, hashvalue, found); |
| } |
| |
| /* ------------------------------------------------------------------------- */ |
| /* Grow: double the capacity and rehash. */ |
| |
| static void |
| agehash_grow(AgeHashTable *t) |
| { |
| char *old_slots; |
| uint32 old_cap; |
| uint32 old_slot_size; |
| uint32 new_cap; |
| MemoryContext oldctx; |
| uint32 i; |
| |
| old_slots = t->slots; |
| old_cap = t->capacity; |
| old_slot_size = t->slot_size; |
| |
| if (t->frozen) |
| { |
| elog(ERROR, "agehash: grow on frozen table"); |
| } |
| Assert(!t->frozen); |
| if (old_cap > (UINT32_MAX >> 1)) |
| { |
| ereport(ERROR, |
| (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), |
| errmsg("agehash capacity overflow: cannot grow beyond %u slots", |
| old_cap))); |
| } |
| new_cap = old_cap << 1; |
| Assert(new_cap > old_cap); /* overflow guard */ |
| |
| oldctx = MemoryContextSwitchTo(t->mcxt); |
| |
| t->capacity = new_cap; |
| t->capacity_mask = new_cap - 1; |
| t->max_size = (uint32) ((double) new_cap * AGEHASH_MAX_LOAD); |
| /* HUGE allocator: see agehash_create_inline for the rationale. */ |
| t->slots = (char *) MemoryContextAllocHuge(t->mcxt, |
| (Size) new_cap * t->slot_size); |
| for (i = 0; i < new_cap; i++) |
| slot_set_probe_dist(slot_at(t, i), AGEHASH_EMPTY); |
| |
| /* Reset size; we re-insert below (which will increment it). */ |
| t->size = 0; |
| for (i = 0; i < old_cap; i++) |
| { |
| char *src = old_slots + (Size) i * old_slot_size; |
| if (slot_probe_dist(src) != AGEHASH_EMPTY) |
| { |
| void *src_key = src + AGEHASH_SLOT_KEY_OFFSET; |
| void *src_payload = src + t->payload_offset; |
| uint32 h = t->hash_fn(src_key, t->key_size); |
| void *dst_payload = agehash_insert_internal(t, src_key, h, NULL); |
| memcpy(dst_payload, src_payload, t->payload_size); |
| } |
| } |
| |
| pfree(old_slots); |
| MemoryContextSwitchTo(oldctx); |
| } |
| |
| /* ------------------------------------------------------------------------- */ |
| /* Lookup. */ |
| |
| void * |
| agehash_lookup_with_hash(AgeHashTable *t, const void *key, uint32 hashvalue) |
| { |
| uint32 i = hashvalue & t->capacity_mask; |
| uint16 d = 0; |
| |
| for (;;) |
| { |
| char *slot = slot_at(t, i); |
| uint16 sd = slot_probe_dist(slot); |
| |
| if (sd == AGEHASH_EMPTY) |
| return NULL; |
| /* |
| * Robin Hood invariant: probe_dist values along a probe sequence |
| * are non-increasing as we move from an entry's home slot. If the |
| * slot we land on has a smaller probe_dist than ours, the key |
| * we're looking for can't be anywhere later in the sequence. |
| */ |
| if (sd < d) |
| return NULL; |
| if (t->keyeq_fn(slot_key_ptr(t, slot), key, t->key_size)) |
| return slot_payload_ptr(t, slot); |
| |
| i = (i + 1) & t->capacity_mask; |
| d++; |
| Assert(d < 0xFE00); |
| } |
| } |
| |
| void * |
| agehash_lookup(AgeHashTable *t, const void *key) |
| { |
| uint32 h = t->hash_fn(key, t->key_size); |
| return agehash_lookup_with_hash(t, key, h); |
| } |
| |
| /* ------------------------------------------------------------------------- */ |
| /* Misc accessors. */ |
| |
| void |
| agehash_freeze(AgeHashTable *t) |
| { |
| t->frozen = true; |
| } |
| |
| bool |
| agehash_is_frozen(const AgeHashTable *t) |
| { |
| return t->frozen; |
| } |
| |
| uint32 |
| agehash_size(const AgeHashTable *t) |
| { |
| return t->size; |
| } |
| |
| uint32 |
| agehash_capacity(const AgeHashTable *t) |
| { |
| return t->capacity; |
| } |
| |
| void |
| agehash_iter_init(AgeHashTable *t, AgeHashIter *it) |
| { |
| it->t = t; |
| it->idx = 0; |
| it->key = NULL; |
| it->payload = NULL; |
| } |
| |
| bool |
| agehash_iter_next(AgeHashIter *it) |
| { |
| AgeHashTable *t = it->t; |
| while (it->idx < t->capacity) |
| { |
| char *slot = slot_at(t, it->idx); |
| uint32 idx = it->idx++; |
| (void) idx; |
| if (slot_probe_dist(slot) != AGEHASH_EMPTY) |
| { |
| it->key = slot_key_ptr(t, slot); |
| it->payload = slot_payload_ptr(t, slot); |
| return true; |
| } |
| } |
| it->key = NULL; |
| it->payload = NULL; |
| return false; |
| } |
| |
| /* ------------------------------------------------------------------------- */ |
| /* Self-test. Exercises insert / lookup / grow / iterate at small + medium |
| * sizes and verifies invariants. Returns a string in CurrentMemoryContext. */ |
| |
| /* MurmurHash3 fmix64, identical to graphid_hash. */ |
| static uint32 |
| selftest_hash(const void *key, Size keysize) |
| { |
| uint64 k; |
| Assert(keysize == sizeof(uint64)); |
| memcpy(&k, key, sizeof(uint64)); |
| k ^= k >> 33; |
| k *= UINT64CONST(0xff51afd7ed558ccd); |
| k ^= k >> 33; |
| k *= UINT64CONST(0xc4ceb9fe1a85ec53); |
| k ^= k >> 33; |
| return (uint32) k; |
| } |
| |
| static bool |
| selftest_keyeq(const void *a, const void *b, Size keysize) |
| { |
| return memcmp(a, b, keysize) == 0; |
| } |
| |
| typedef struct selftest_payload |
| { |
| uint64 mirror_key; |
| uint64 marker; |
| } selftest_payload; |
| |
| static const char * |
| selftest_run_one(MemoryContext parent, uint32 n, uint32 hint) |
| { |
| MemoryContext mcxt; |
| AgeHashTable *t; |
| selftest_payload *p; |
| bool found; |
| uint32 i; |
| uint32 seen; |
| AgeHashIter it; |
| |
| mcxt = AllocSetContextCreate(parent, "agehash selftest", ALLOCSET_DEFAULT_SIZES); |
| t = agehash_create_inline(mcxt, sizeof(uint64), sizeof(selftest_payload), |
| hint, selftest_hash, selftest_keyeq); |
| |
| /* Insert n keys. */ |
| for (i = 0; i < n; i++) |
| { |
| uint64 k = ((uint64) 0xa5a5 << 48) | (i + 1); |
| p = (selftest_payload *) agehash_insert(t, &k, &found); |
| if (found) |
| { |
| MemoryContextDelete(mcxt); |
| return psprintf("FAIL: duplicate insert at i=%u", i); |
| } |
| p->mirror_key = k; |
| p->marker = (uint64) 0xdeadbeef00000000ULL | i; |
| } |
| if (agehash_size(t) != n) |
| { |
| MemoryContextDelete(mcxt); |
| return psprintf("FAIL: size %u != %u after inserts", |
| agehash_size(t), n); |
| } |
| |
| /* Lookup all n keys. */ |
| for (i = 0; i < n; i++) |
| { |
| uint64 k = ((uint64) 0xa5a5 << 48) | (i + 1); |
| p = (selftest_payload *) agehash_lookup(t, &k); |
| if (p == NULL) |
| { |
| MemoryContextDelete(mcxt); |
| return psprintf("FAIL: lookup miss at i=%u", i); |
| } |
| if (p->mirror_key != k || |
| p->marker != ((uint64) 0xdeadbeef00000000ULL | i)) |
| { |
| MemoryContextDelete(mcxt); |
| return psprintf("FAIL: payload corruption at i=%u", i); |
| } |
| } |
| |
| /* Lookup n keys that should not exist. */ |
| for (i = 0; i < n; i++) |
| { |
| uint64 k = ((uint64) 0xb6b6 << 48) | (i + 1); |
| p = (selftest_payload *) agehash_lookup(t, &k); |
| if (p != NULL) |
| { |
| MemoryContextDelete(mcxt); |
| return psprintf("FAIL: phantom lookup hit at i=%u", i); |
| } |
| } |
| |
| /* Re-insert (HASH_ENTER semantics) — should report found = true. */ |
| for (i = 0; i < n; i++) |
| { |
| uint64 k = ((uint64) 0xa5a5 << 48) | (i + 1); |
| p = (selftest_payload *) agehash_insert(t, &k, &found); |
| if (!found) |
| { |
| MemoryContextDelete(mcxt); |
| return psprintf("FAIL: re-insert reported !found at i=%u", i); |
| } |
| if (p->mirror_key != k) |
| { |
| MemoryContextDelete(mcxt); |
| return psprintf("FAIL: re-insert payload mismatch at i=%u", i); |
| } |
| } |
| if (agehash_size(t) != n) |
| { |
| MemoryContextDelete(mcxt); |
| return psprintf("FAIL: size %u != %u after re-inserts", |
| agehash_size(t), n); |
| } |
| |
| /* Iterate and count. */ |
| seen = 0; |
| agehash_iter_init(t, &it); |
| while (agehash_iter_next(&it)) |
| { |
| selftest_payload *pp = it.payload; |
| uint64 k; |
| memcpy(&k, it.key, sizeof(uint64)); |
| if (pp->mirror_key != k) |
| { |
| MemoryContextDelete(mcxt); |
| return psprintf("FAIL: iter payload mismatch at seen=%u", seen); |
| } |
| seen++; |
| } |
| if (seen != n) |
| { |
| MemoryContextDelete(mcxt); |
| return psprintf("FAIL: iter saw %u of %u", seen, n); |
| } |
| |
| /* Freeze and confirm lookups still work. */ |
| agehash_freeze(t); |
| if (!agehash_is_frozen(t)) |
| { |
| MemoryContextDelete(mcxt); |
| return "FAIL: agehash_is_frozen returned false after freeze"; |
| } |
| { |
| uint64 k = ((uint64) 0xa5a5 << 48) | 1; |
| p = (selftest_payload *) agehash_lookup(t, &k); |
| if (p == NULL) |
| { |
| MemoryContextDelete(mcxt); |
| return "FAIL: lookup failed after freeze"; |
| } |
| } |
| |
| MemoryContextDelete(mcxt); |
| return NULL; /* OK */ |
| } |
| |
| const char * |
| agehash_self_test(void) |
| { |
| static const struct { uint32 n; uint32 hint; } cases[] = { |
| { 1, 0 }, |
| { 7, 0 }, |
| { 8, 0 }, |
| { 9, 0 }, |
| { 63, 0 }, |
| { 64, 0 }, |
| { 65, 0 }, |
| { 1023, 0 }, /* forces grow from 64 floor */ |
| { 1024, 0 }, |
| { 1025, 0 }, |
| { 10000, 0 }, /* forces multiple grows */ |
| { 10000, 8192 }, /* with capacity hint, no grow expected */ |
| { 50000, 0 }, /* larger; multiple grows */ |
| { 1000000, 0 }, /* exercises grow at multi-MB allocations */ |
| /* |
| * NOTE: this set is bounded so 'make installcheck' completes |
| * quickly. The library has been manually verified up to 256M |
| * entries (multi-GiB slot arrays via MemoryContextAllocHuge). |
| */ |
| }; |
| const size_t ncases = sizeof(cases) / sizeof(cases[0]); |
| size_t i; |
| |
| for (i = 0; i < ncases; i++) |
| { |
| const char *r = selftest_run_one(CurrentMemoryContext, |
| cases[i].n, cases[i].hint); |
| if (r != NULL) |
| return psprintf("%s [n=%u hint=%u]", r, cases[i].n, cases[i].hint); |
| } |
| return "OK"; |
| } |
| |
| /* ------------------------------------------------------------------------- */ |
| /* SQL-callable wrapper: SELECT ag_catalog._agehash_self_test(); */ |
| |
| PG_FUNCTION_INFO_V1(_agehash_self_test); |
| |
| Datum |
| _agehash_self_test(PG_FUNCTION_ARGS) |
| { |
| const char *r = agehash_self_test(); |
| PG_RETURN_TEXT_P(cstring_to_text(r)); |
| } |