mirror of
https://github.com/shadowsocks/shadowsocks-libev.git
synced 2026-10-20 13:03:50 +00:00
cache.cpp keeps its C API but replaces uthash and the two mallocs per entry with one slab of nodes holding their keys inline, linked in last-use order, and an open-addressing index of node ids that hashes through the slab with a per-cache random seed. Both blocks are reserved by cache_create() and touched only as entries are used, so a large, mostly empty cache costs no resident memory. Semantics are unchanged: an insert evicts the least recently used entry once the cache holds capacity entries (the AEAD-2022 session table's 4095/4096 boundary), lookups refresh recency and age, cache_clear() drops stale entries, and free_cb may re-enter the cache. Re-inserting a key now replaces its data instead of shadowing it. cache_create_keyed() sizes keys per cache; the AEAD-2022 salt pool and session table use it. test_cache_cpp covers the boundary, ordering, replacement, ageing, re-entrant callbacks and limits; alloc_steady_cache requires zero allocations across 100,000 inserts on a full cache. AEAD-2022 ss-server drops from 84 to 80 allocations per relayed connection.
167 lines
5.3 KiB
C++
167 lines
5.3 KiB
C++
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
|
// Edge cases of the fixed-capacity cache (src/cache.cpp) beyond
|
|
// test_cache.c, and proof that a full cache churns without allocating.
|
|
#include <cerrno>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
|
|
#include "alloc_audit/alloc_probe.hpp"
|
|
#include "cache.h"
|
|
#include "test_helpers.hpp"
|
|
|
|
extern "C" {
|
|
int verbose = 0;
|
|
}
|
|
|
|
namespace {
|
|
|
|
int g_freed = 0;
|
|
struct cache *g_victim = nullptr;
|
|
|
|
void count_free(void *, void *) { ++g_freed; }
|
|
|
|
// Re-enters the cache it belongs to, as udprelay's callbacks may.
|
|
void reentrant_free(void *key, void *)
|
|
{
|
|
++g_freed;
|
|
char probe[] = "other";
|
|
(void)cache_key_exist(g_victim, probe, 5);
|
|
(void)cache_remove(g_victim, static_cast<char *>(key), std::strlen(static_cast<char *>(key)));
|
|
}
|
|
|
|
void key(char (&buf)[16], int i)
|
|
{
|
|
std::snprintf(buf, sizeof(buf), "k%d", i);
|
|
}
|
|
|
|
void test_capacity_boundary()
|
|
{
|
|
// The AEAD-2022 session table relies on this: a cache of N keeps N - 1.
|
|
struct cache *c = nullptr;
|
|
SS_CHECK(cache_create(&c, 4096, count_free) == 0);
|
|
g_freed = 0;
|
|
static int token;
|
|
char k[16];
|
|
for (int i = 0; i < 4095; ++i) {
|
|
key(k, i);
|
|
SS_CHECK(cache_insert(c, k, std::strlen(k), &token) == 0);
|
|
}
|
|
SS_CHECK(cache_size(c) == 4095 && g_freed == 0);
|
|
key(k, 4095);
|
|
SS_CHECK(cache_insert(c, k, std::strlen(k), &token) == 0);
|
|
SS_CHECK(cache_size(c) == 4095 && g_freed == 1);
|
|
key(k, 0);
|
|
SS_CHECK(cache_key_exist(c, k, std::strlen(k)) == 0); // the oldest went
|
|
SS_CHECK(cache_delete(c, 0) == 0);
|
|
SS_CHECK(g_freed == 4096);
|
|
}
|
|
|
|
void test_lru_order_and_replace()
|
|
{
|
|
struct cache *c = nullptr;
|
|
SS_CHECK(cache_create(&c, 4, count_free) == 0);
|
|
g_freed = 0;
|
|
static int a, b, d, e, f;
|
|
SS_CHECK(cache_insert(c, (char *)"a", 1, &a) == 0);
|
|
SS_CHECK(cache_insert(c, (char *)"b", 1, &b) == 0);
|
|
SS_CHECK(cache_insert(c, (char *)"d", 1, &d) == 0);
|
|
// Touching "a" makes "b" the eviction candidate.
|
|
void *found = nullptr;
|
|
SS_CHECK(cache_lookup(c, (char *)"a", 1, &found) == 0 && found == &a);
|
|
SS_CHECK(cache_insert(c, (char *)"e", 1, &e) == 0);
|
|
SS_CHECK(cache_key_exist(c, (char *)"b", 1) == 0);
|
|
SS_CHECK(cache_key_exist(c, (char *)"a", 1) == 1);
|
|
// Re-inserting replaces the data and releases the old one.
|
|
SS_CHECK(cache_insert(c, (char *)"a", 1, &f) == 0);
|
|
SS_CHECK(cache_lookup(c, (char *)"a", 1, &found) == 0 && found == &f);
|
|
SS_CHECK(g_freed == 2);
|
|
SS_CHECK(cache_lookup(c, (char *)"zz", 2, &found) == 0 && found == nullptr);
|
|
// keep_data skips the callbacks.
|
|
SS_CHECK(cache_delete(c, 1) == 0);
|
|
SS_CHECK(g_freed == 2);
|
|
}
|
|
|
|
void test_clear_by_age()
|
|
{
|
|
struct cache *c = nullptr;
|
|
SS_CHECK(cache_create(&c, 8, count_free) == 0);
|
|
g_freed = 0;
|
|
static int x;
|
|
SS_CHECK(cache_insert(c, (char *)"old", 3, &x) == 0);
|
|
SS_CHECK(cache_clear(c, 3600) == 0);
|
|
SS_CHECK(cache_size(c) == 1 && g_freed == 0);
|
|
SS_CHECK(cache_clear(c, -1) == 0); // everything is older than -1 s
|
|
SS_CHECK(cache_size(c) == 0 && g_freed == 1);
|
|
SS_CHECK(cache_delete(c, 0) == 0);
|
|
}
|
|
|
|
void test_reentrant_callback_and_limits()
|
|
{
|
|
struct cache *c = nullptr;
|
|
SS_CHECK(cache_create_keyed(&c, 2, 8, reentrant_free) == 0);
|
|
g_victim = c;
|
|
g_freed = 0;
|
|
static int x;
|
|
SS_CHECK(cache_insert(c, (char *)"one", 3, &x) == 0);
|
|
SS_CHECK(cache_insert(c, (char *)"two", 3, &x) == 0); // evicts "one" -> callback re-enters
|
|
SS_CHECK(g_freed == 1 && cache_size(c) == 1);
|
|
SS_CHECK(cache_insert(c, (char *)"too-long-key", 12, &x) == EINVAL);
|
|
SS_CHECK(cache_create_keyed(&c, 2, 0, nullptr) == EINVAL);
|
|
SS_CHECK(cache_create_keyed(&c, 2, 256, nullptr) == EINVAL);
|
|
SS_CHECK(cache_delete(g_victim, 0) == 0);
|
|
SS_CHECK(cache_insert(nullptr, (char *)"x", 1, &x) == EINVAL);
|
|
}
|
|
|
|
void test_zero_capacity()
|
|
{
|
|
// Matches the uthash version: every insert evicts immediately.
|
|
struct cache *c = nullptr;
|
|
SS_CHECK(cache_create(&c, 0, count_free) == 0);
|
|
g_freed = 0;
|
|
static int x;
|
|
SS_CHECK(cache_insert(c, (char *)"a", 1, &x) == 0);
|
|
SS_CHECK(cache_insert(c, (char *)"b", 1, &x) == 0);
|
|
SS_CHECK(cache_size(c) == 0 && g_freed == 2);
|
|
SS_CHECK(cache_delete(c, 0) == 0);
|
|
}
|
|
|
|
void test_steady_churn_without_allocation()
|
|
{
|
|
struct cache *c = nullptr;
|
|
SS_CHECK(cache_create(&c, 512, count_free) == 0);
|
|
static int x;
|
|
char k[16];
|
|
for (int i = 0; i < 2048; ++i) { // warm-up touches every node
|
|
key(k, i);
|
|
SS_CHECK(cache_insert(c, k, std::strlen(k), &x) == 0);
|
|
}
|
|
ss::test::alloc_mark();
|
|
void *found = nullptr;
|
|
for (int i = 0; i < 100000; ++i) {
|
|
key(k, i);
|
|
SS_CHECK(cache_insert(c, k, std::strlen(k), &x) == 0);
|
|
SS_CHECK(cache_lookup(c, k, std::strlen(k), &found) == 0 && found == &x);
|
|
if (i % 7 == 0) {
|
|
SS_CHECK(cache_remove(c, k, std::strlen(k)) == 0);
|
|
}
|
|
}
|
|
(void)cache_clear(c, -1);
|
|
ss::test::alloc_mark();
|
|
SS_CHECK(cache_size(c) == 0);
|
|
SS_CHECK(cache_delete(c, 0) == 0);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main()
|
|
{
|
|
SS_RUN(test_capacity_boundary);
|
|
SS_RUN(test_lru_order_and_replace);
|
|
SS_RUN(test_clear_by_age);
|
|
SS_RUN(test_reentrant_callback_and_limits);
|
|
SS_RUN(test_zero_capacity);
|
|
SS_RUN(test_steady_churn_without_allocation);
|
|
return 0;
|
|
}
|