0ad-0001 CCmpObstructionManager dirty shapes vector+std::find O(N*D) HIGH 4.1x 0ad-0002 CCmpRangeManager m_ModifiedEntities vector+std::find O(E*M) HIGH 25.8x 0ad-0003 CCmpTemplateManager FindUsedTemplates vector+std::find O(T^2) MEDIUM 5.9x 0ad-0004 XmppClient+NetServer lobby auth token logged verbatim CWE-312 MEDIUM MOAD-0002 (Intertangle): g_ globals are deliberate single-thread game arch, CLEAN MOAD-0003 (Leaked Context): thread_local properly scoped, CLEAN MOAD-0005 (Thundering Herd): single-threaded sim, no cache stampede, CLEAN 4/4 unit tests PASS, UNDF 956-959
148 lines
5.3 KiB
C++
148 lines
5.3 KiB
C++
// Unit test for 0ad-0001: CCmpObstructionManager dirty shape dedup
|
|
// Verifies that unordered_set-based dedup matches vector-based behavior
|
|
// and demonstrates O(N^2) -> O(N) improvement.
|
|
|
|
#include <vector>
|
|
#include <unordered_set>
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <cstdio>
|
|
#include <cstdint>
|
|
#include <cassert>
|
|
|
|
using u32 = uint32_t;
|
|
|
|
// --- BEFORE: vector + std::find dedup ---
|
|
struct DirtyTrackerBefore {
|
|
std::vector<u32> m_DirtyStaticShapes;
|
|
std::vector<u32> m_DirtyUnitShapes;
|
|
|
|
void MakeDirtyStatic(u32 index, const std::vector<u32>& nearbyStatics, const std::vector<u32>& nearbyUnits) {
|
|
if (std::find(m_DirtyStaticShapes.begin(), m_DirtyStaticShapes.end(), index) == m_DirtyStaticShapes.end())
|
|
m_DirtyStaticShapes.push_back(index);
|
|
for (u32 staticId : nearbyStatics)
|
|
if (std::find(m_DirtyStaticShapes.begin(), m_DirtyStaticShapes.end(), staticId) == m_DirtyStaticShapes.end())
|
|
m_DirtyStaticShapes.push_back(staticId);
|
|
for (u32 unitId : nearbyUnits)
|
|
if (std::find(m_DirtyUnitShapes.begin(), m_DirtyUnitShapes.end(), unitId) == m_DirtyUnitShapes.end())
|
|
m_DirtyUnitShapes.push_back(unitId);
|
|
}
|
|
|
|
bool IsDirtyStatic(u32 id) const {
|
|
return std::find(m_DirtyStaticShapes.begin(), m_DirtyStaticShapes.end(), id) != m_DirtyStaticShapes.end();
|
|
}
|
|
|
|
bool IsDirtyUnit(u32 id) const {
|
|
return std::find(m_DirtyUnitShapes.begin(), m_DirtyUnitShapes.end(), id) != m_DirtyUnitShapes.end();
|
|
}
|
|
};
|
|
|
|
// --- AFTER: unordered_set dedup ---
|
|
struct DirtyTrackerAfter {
|
|
std::unordered_set<u32> m_DirtyStaticShapes;
|
|
std::unordered_set<u32> m_DirtyUnitShapes;
|
|
|
|
void MakeDirtyStatic(u32 index, const std::vector<u32>& nearbyStatics, const std::vector<u32>& nearbyUnits) {
|
|
m_DirtyStaticShapes.insert(index);
|
|
for (u32 staticId : nearbyStatics)
|
|
m_DirtyStaticShapes.insert(staticId);
|
|
for (u32 unitId : nearbyUnits)
|
|
m_DirtyUnitShapes.insert(unitId);
|
|
}
|
|
|
|
bool IsDirtyStatic(u32 id) const {
|
|
return m_DirtyStaticShapes.count(id) > 0;
|
|
}
|
|
|
|
bool IsDirtyUnit(u32 id) const {
|
|
return m_DirtyUnitShapes.count(id) > 0;
|
|
}
|
|
};
|
|
|
|
void test_correctness() {
|
|
DirtyTrackerBefore before;
|
|
DirtyTrackerAfter after;
|
|
|
|
// Simulate dirtying shapes in a battle scenario
|
|
std::vector<u32> nearbyStatics = {10, 20, 30, 40, 50};
|
|
std::vector<u32> nearbyUnits = {100, 200, 300};
|
|
|
|
// Multiple dirty calls with overlapping IDs (common in RTS battles)
|
|
for (u32 i = 0; i < 20; ++i) {
|
|
before.MakeDirtyStatic(i, nearbyStatics, nearbyUnits);
|
|
after.MakeDirtyStatic(i, nearbyStatics, nearbyUnits);
|
|
}
|
|
|
|
// Duplicate calls (units moving repeatedly)
|
|
for (u32 i = 0; i < 20; ++i) {
|
|
before.MakeDirtyStatic(i, nearbyStatics, nearbyUnits);
|
|
after.MakeDirtyStatic(i, nearbyStatics, nearbyUnits);
|
|
}
|
|
|
|
// Verify same sets
|
|
assert(before.m_DirtyStaticShapes.size() == after.m_DirtyStaticShapes.size());
|
|
assert(before.m_DirtyUnitShapes.size() == after.m_DirtyUnitShapes.size());
|
|
|
|
for (u32 id : before.m_DirtyStaticShapes)
|
|
assert(after.IsDirtyStatic(id));
|
|
for (u32 id : before.m_DirtyUnitShapes)
|
|
assert(after.IsDirtyUnit(id));
|
|
|
|
printf("PASS correctness: sets match (%zu statics, %zu units)\n",
|
|
after.m_DirtyStaticShapes.size(), after.m_DirtyUnitShapes.size());
|
|
}
|
|
|
|
void test_performance() {
|
|
const int N = 2000; // shapes moving per frame in a 200v200 battle
|
|
const int NEARBY = 20; // avg nearby shapes per dirty call
|
|
|
|
// Build nearby lists
|
|
std::vector<u32> nearbyStatics(NEARBY);
|
|
std::vector<u32> nearbyUnits(NEARBY);
|
|
for (int i = 0; i < NEARBY; ++i) {
|
|
nearbyStatics[i] = 1000 + i;
|
|
nearbyUnits[i] = 2000 + i;
|
|
}
|
|
|
|
// BEFORE
|
|
auto t0 = std::chrono::high_resolution_clock::now();
|
|
DirtyTrackerBefore before;
|
|
for (int i = 0; i < N; ++i)
|
|
before.MakeDirtyStatic(i, nearbyStatics, nearbyUnits);
|
|
// Simulate RasterizeHelper dirty check
|
|
for (int i = 0; i < N + NEARBY; ++i) {
|
|
before.IsDirtyStatic(i);
|
|
before.IsDirtyUnit(i);
|
|
}
|
|
auto t1 = std::chrono::high_resolution_clock::now();
|
|
|
|
// AFTER
|
|
auto t2 = std::chrono::high_resolution_clock::now();
|
|
DirtyTrackerAfter after;
|
|
for (int i = 0; i < N; ++i)
|
|
after.MakeDirtyStatic(i, nearbyStatics, nearbyUnits);
|
|
for (int i = 0; i < N + NEARBY; ++i) {
|
|
after.IsDirtyStatic(i);
|
|
after.IsDirtyUnit(i);
|
|
}
|
|
auto t3 = std::chrono::high_resolution_clock::now();
|
|
|
|
double before_us = std::chrono::duration<double, std::micro>(t1 - t0).count();
|
|
double after_us = std::chrono::duration<double, std::micro>(t3 - t2).count();
|
|
double ratio = before_us / after_us;
|
|
|
|
printf("PASS performance: before=%.0fus after=%.0fus ratio=%.1fx (N=%d, nearby=%d)\n",
|
|
before_us, after_us, ratio, N, NEARBY);
|
|
// Op-count analysis: before does N*D finds where D grows to N, after does N inserts O(1) each
|
|
// At O2, the compiler may optimize small vectors, but op-count ratio is clear
|
|
printf(" op-count: before=%d vector-finds, after=%d set-inserts\n",
|
|
N * (N/2 + NEARBY), N * (1 + NEARBY));
|
|
assert(ratio > 1.5 && "Expected at least 1.5x speedup");
|
|
}
|
|
|
|
int main() {
|
|
test_correctness();
|
|
test_performance();
|
|
printf("ALL TESTS PASSED\n");
|
|
return 0;
|
|
}
|