java-topology/defects/netpanzer-0001/test/test_unit_removal.cpp
russell@unturf.com 8c8f896efc netpanzer: 2 CWE-407 defects, MOAD 0002-0005 CLEAN
netpanzer-0001: UnitInterface::removeUnit std::find on PlayerUnitList
  vector O(D*U) during mass destruction. Fix: unordered_map index +
  swap-and-pop O(1) removal. MEDIUM, 3.7x at U=2000 D=1000.

netpanzer-0002: UnitBucketArray::getUnitBucketIndex scans all buckets
  O(B*U) per misplaced unit in sortBucketArray fallback path. Fix:
  unordered_map<UnitID, bucket_index> for O(1) lookup. HIGH, 9.4x
  at B=200 U=2000 M=500.

MOAD 0003 (ThreadLocal): CLEAN, no thread_local patterns
MOAD 0004 (Logged Secret): CLEAN, passwords not logged verbatim
MOAD 0005 (Thundering Herd): CLEAN, PathCache is single-threaded
2026-03-31 12:58:12 -04:00

157 lines
4.8 KiB
C++

// Unit test for netpanzer-0001: UnitInterface::removeUnit std::find O(U) -> O(1) swap-and-pop
// CWE-407: Algorithmic Complexity — list membership inside removal loop
//
// Defect: removeUnit() calls std::find(plist.begin(), plist.end(), unit) to locate
// a unit in our per-player vector before erasing it. std::find is O(U) where U is
// our player's unit count. When multiple units are destroyed in a single frame
// (mass battle), updateUnitStatus calls removeUnit for each dead unit, yielding
// O(D*U) total where D is dead units per frame.
//
// Fix: maintain a parallel std::unordered_map<UnitBase*, size_t> tracking each
// unit's index in our player vector. Removal becomes O(1) via swap-with-back
// plus pop_back, eliminating our linear scan entirely.
#include <algorithm>
#include <cassert>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <unordered_map>
#include <vector>
// Simulate our UnitBase* as opaque pointers
struct FakeUnit {
int id;
};
// ========== DEFECTIVE VERSION: std::find + erase ==========
struct DefectivePlayerList {
std::vector<FakeUnit*> units;
void addUnit(FakeUnit* u) {
units.push_back(u);
}
void removeUnit(FakeUnit* u) {
auto it = std::find(units.begin(), units.end(), u);
if (it != units.end()) {
units.erase(it); // O(U) find + O(U) shift
}
}
};
// ========== FIXED VERSION: index map + swap-and-pop ==========
struct FixedPlayerList {
std::vector<FakeUnit*> units;
std::unordered_map<FakeUnit*, size_t> indexMap;
void addUnit(FakeUnit* u) {
indexMap[u] = units.size();
units.push_back(u);
}
void removeUnit(FakeUnit* u) {
auto it = indexMap.find(u);
if (it != indexMap.end()) {
size_t idx = it->second;
if (idx < units.size() - 1) {
FakeUnit* back = units.back();
units[idx] = back;
indexMap[back] = idx;
}
units.pop_back();
indexMap.erase(it);
}
}
};
static long long now_ns() {
return std::chrono::high_resolution_clock::now().time_since_epoch().count();
}
int main() {
// Test correctness first
{
FixedPlayerList fixed;
std::vector<FakeUnit> pool(100);
for (int i = 0; i < 100; i++) {
pool[i].id = i;
fixed.addUnit(&pool[i]);
}
assert(fixed.units.size() == 100);
// Remove every other unit
for (int i = 0; i < 100; i += 2) {
fixed.removeUnit(&pool[i]);
}
assert(fixed.units.size() == 50);
// Verify all remaining units are odd-indexed
for (size_t i = 0; i < fixed.units.size(); i++) {
assert(fixed.units[i]->id % 2 == 1);
// Verify index map is consistent
assert(fixed.indexMap[fixed.units[i]] == i);
}
// Remove all remaining
std::vector<FakeUnit*> remaining(fixed.units.begin(), fixed.units.end());
for (auto* u : remaining) {
fixed.removeUnit(u);
}
assert(fixed.units.size() == 0);
assert(fixed.indexMap.size() == 0);
}
printf("PASS correctness\n");
// Benchmark: simulate mass destruction (many removals from large list)
const int U = 2000; // units per player
const int D = 1000; // units destroyed per frame
std::vector<FakeUnit> units(U);
for (int i = 0; i < U; i++) units[i].id = i;
// Build removal order (first D units)
std::vector<int> removeOrder(D);
for (int i = 0; i < D; i++) removeOrder[i] = i;
const int TRIALS = 200;
// Benchmark defective
long long defective_ns = 0;
for (int t = 0; t < TRIALS; t++) {
DefectivePlayerList defective;
for (int i = 0; i < U; i++) defective.addUnit(&units[i]);
long long start = now_ns();
for (int i = 0; i < D; i++) {
defective.removeUnit(&units[removeOrder[i]]);
}
defective_ns += now_ns() - start;
}
// Benchmark fixed
long long fixed_ns = 0;
for (int t = 0; t < TRIALS; t++) {
FixedPlayerList fixed;
for (int i = 0; i < U; i++) fixed.addUnit(&units[i]);
long long start = now_ns();
for (int i = 0; i < D; i++) {
fixed.removeUnit(&units[removeOrder[i]]);
}
fixed_ns += now_ns() - start;
}
double ratio = (double)defective_ns / (double)fixed_ns;
printf("Defective: %lld ns total (%d trials)\n", defective_ns, TRIALS);
printf("Fixed: %lld ns total (%d trials)\n", fixed_ns, TRIALS);
printf("Ratio: %.1fx speedup\n", ratio);
printf("U=%d units, D=%d destroyed per frame\n", U, D);
// At U=500, D=250, we expect significant speedup
assert(ratio > 2.0 && "Fixed version should be at least 2x faster");
printf("PASS performance (%.1fx)\n", ratio);
return 0;
}