java-topology/defects/xash3d-0002/test/test_precache_index.c
russell@unturf.com 826a18b121 simutrans: 3 CWE-407 defects in halt reconnection, MOAD 0002-0005 CLEAN
simutrans-0001: rebuild_linked_connections() append_unique O(C*H^2) MEDIUM 97x
  - vector_tpl::append_unique linear scan inside double loop over
    goods categories x connections to collect unique connected halts
  - fix: inthashtable_tpl for O(1) membership test

simutrans-0002: add_grund() registered_convoys.is_contained O(C*R) MEDIUM 45x
  - iterates ALL world convoys, each with linear scan of registered
    convoy vector to check membership
  - fix: pre-build hash set of registered convoy IDs for O(1) lookup

simutrans-0003: rebuild_connections() consecutive_halts append_unique O(S^2) MEDIUM 24x
  - append_unique on consecutive halt vectors per category inside
    nested loop over schedules x entries during halt reconnection
  - fix: parallel inthashtable_tpl for O(1) dedup

All three defects are in simhalt.cc halt reconnection paths, triggered
whenever schedules change (line added/removed, schedule edited, station
built). In large games with hundreds of halts and convoys, these
compound during reconnection sweeps.

MOAD-0002: welt (karte_t) is a god object but standard Simutrans architecture
MOAD-0003: CLEAN (no thread_local usage)
MOAD-0004: CLEAN (nettool password printf is by-design tool output)
MOAD-0005: CLEAN (save cache uses hashtable, no unsynchronized pattern)
2026-03-31 13:01:22 -04:00

185 lines
4.8 KiB
C

/*
* test_precache_index.c
*
* Unit test for xash3d-0002: SV_ModelIndex/SV_SoundIndex O(N^2) precache scan
*
* Demonstrates that precache-index functions with linear dedup scan produce
* O(N^2/2) string comparisons when registering N unique resources, and that
* a hash-based approach reduces this to O(N) amortized.
*
* Defect: SV_ModelIndex scans model_precache[1..i] for each new precache
* call. With N unique models, total comparisons = 1+2+3+...+N = N*(N+1)/2.
* At MAX_MODELS=4096, that is ~8.4M string comparisons per map load.
*
* Fix: Maintain a hash table parallel to our precache array for O(1) lookup.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_MODELS_TEST 2048
#define MAX_QPATH 64
#define HASH_SIZE 512
/* --- Simulate defective linear version --- */
static char model_precache[MAX_MODELS_TEST][MAX_QPATH];
static int num_models = 0;
static long linear_comparisons = 0;
static int model_index_linear(const char *name)
{
int i;
for (i = 0; i < num_models; i++)
{
linear_comparisons++;
if (strcmp(model_precache[i], name) == 0)
return i + 1;
}
/* Register new */
if (num_models < MAX_MODELS_TEST)
{
strncpy(model_precache[num_models], name, MAX_QPATH - 1);
model_precache[num_models][MAX_QPATH - 1] = '\0';
num_models++;
}
return num_models;
}
/* --- Simulate patched hash version --- */
typedef struct hash_entry_s {
int index;
struct hash_entry_s *next;
} hash_entry_t;
static hash_entry_t *htable[HASH_SIZE];
static hash_entry_t hpool[MAX_MODELS_TEST];
static int hpool_used = 0;
static char model_precache_h[MAX_MODELS_TEST][MAX_QPATH];
static int num_models_h = 0;
static long hash_comparisons = 0;
static unsigned int hash_key(const char *s)
{
unsigned int h = 0;
while (*s)
{
h = h * 31 + (unsigned char)*s;
s++;
}
return h % HASH_SIZE;
}
static int model_index_hash(const char *name)
{
unsigned int h = hash_key(name);
hash_entry_t *e;
for (e = htable[h]; e != NULL; e = e->next)
{
hash_comparisons++;
if (strcmp(model_precache_h[e->index], name) == 0)
return e->index + 1;
}
/* Register new */
if (num_models_h < MAX_MODELS_TEST)
{
int idx = num_models_h;
strncpy(model_precache_h[idx], name, MAX_QPATH - 1);
model_precache_h[idx][MAX_QPATH - 1] = '\0';
num_models_h++;
hash_entry_t *ne = &hpool[hpool_used++];
ne->index = idx;
ne->next = htable[h];
htable[h] = ne;
}
return num_models_h;
}
/* --- Test --- */
int main(void)
{
int i, N;
char names[MAX_MODELS_TEST][MAX_QPATH];
double ratio;
int pass = 1;
int result_linear, result_hash;
N = 1500; /* Typical heavy mod precache count */
/* Generate N unique model names */
for (i = 0; i < N; i++)
snprintf(names[i], MAX_QPATH, "models/entity_%04d.mdl", i);
/* Reset state */
memset(model_precache, 0, sizeof(model_precache));
memset(model_precache_h, 0, sizeof(model_precache_h));
memset(htable, 0, sizeof(htable));
num_models = 0;
num_models_h = 0;
hpool_used = 0;
linear_comparisons = 0;
hash_comparisons = 0;
/* Simulate precaching N unique models (each one is new) */
for (i = 0; i < N; i++)
model_index_linear(names[i]);
for (i = 0; i < N; i++)
model_index_hash(names[i]);
/* Verify both produce same count */
if (num_models != num_models_h)
{
fprintf(stderr, "FAIL: model count mismatch: linear=%d hash=%d\n",
num_models, num_models_h);
pass = 0;
}
/* Verify lookups produce same results */
for (i = 0; i < N; i++)
{
/* Reset counters for correctness check (not counting these) */
result_linear = model_index_linear(names[i]);
result_hash = model_index_hash(names[i]);
if (result_linear != result_hash)
{
fprintf(stderr, "FAIL: index mismatch for %s: linear=%d hash=%d\n",
names[i], result_linear, result_hash);
pass = 0;
break;
}
}
ratio = (double)linear_comparisons / (hash_comparisons > 0 ? hash_comparisons : 1);
printf("xash3d-0002: SV_ModelIndex O(N^2/2) -> O(N) precache dedup\n");
printf(" N=%d unique models\n", N);
printf(" Linear comparisons: %ld (expected ~N^2/2 = %ld)\n",
linear_comparisons, (long)N * (N - 1) / 2);
printf(" Hash comparisons: %ld\n", hash_comparisons);
printf(" Ratio: %.1fx\n", ratio);
if (ratio < 10.0)
{
fprintf(stderr, "FAIL: expected at least 10x improvement, got %.1fx\n", ratio);
pass = 0;
}
if (pass)
{
printf("PASS\n");
return 0;
}
else
{
printf("FAIL\n");
return 1;
}
}