diff --git a/defects/bzflag-0001/patch/bzflag-0001.patch b/defects/bzflag-0001/patch/bzflag-0001.patch new file mode 100644 index 000000000..f96e8e9fa --- /dev/null +++ b/defects/bzflag-0001/patch/bzflag-0001.patch @@ -0,0 +1,60 @@ +--- a/include/WorldEventManager.h ++++ b/include/WorldEventManager.h +@@ -24,8 +24,8 @@ + #include "common.h" + + // System headers ++#include + #include +-#include + #include + + // Common headers +@@ -48,22 +48,27 @@ + plugin->Event(eventData); + } + +- std::vector HandledEvents; ++ std::bitset handledEventBits; + + bool HasEvent( bz_eEventType evt) + { +- return std::find(HandledEvents.begin(),HandledEvents.end(),evt) != HandledEvents.end(); ++ return (evt >= 0 && evt < bz_eLastEvent) && handledEventBits.test(evt); + } + + void AddEvent( bz_eEventType evt ) + { +- if (std::find(HandledEvents.begin(),HandledEvents.end(),evt) == HandledEvents.end()) +- HandledEvents.push_back(evt); ++ if (evt >= 0 && evt < bz_eLastEvent) ++ handledEventBits.set(evt); + } + + void RemoveEvent( bz_eEventType evt ) + { +- std::vector::iterator itr = std::find(HandledEvents.begin(),HandledEvents.end(),evt); +- +- if ( itr!= HandledEvents.end()) +- HandledEvents.erase(itr); ++ if (evt >= 0 && evt < bz_eLastEvent) ++ handledEventBits.reset(evt); ++ } ++ ++ bool HasNoEvents() const ++ { ++ return handledEventBits.none(); + } + }; + +--- a/src/bzfs/WorldEventManager.cxx ++++ b/src/bzfs/WorldEventManager.cxx +@@ -147,7 +147,7 @@ + bz_EventHandler *handler = HandlerMap[plugin]; + worldEventManager.removeEvent(eventType,handler); + +- if (handler->HandledEvents.empty()) ++ if (handler->HasNoEvents()) + worldEventManager.removeHandler(handler); + + return true; diff --git a/defects/bzflag-0001/test/test b/defects/bzflag-0001/test/test new file mode 100755 index 000000000..4b08ca9d8 Binary files /dev/null and b/defects/bzflag-0001/test/test differ diff --git a/defects/bzflag-0001/test/test_event_handler_hasEvent.cpp b/defects/bzflag-0001/test/test_event_handler_hasEvent.cpp new file mode 100644 index 000000000..302a9d8e2 --- /dev/null +++ b/defects/bzflag-0001/test/test_event_handler_hasEvent.cpp @@ -0,0 +1,245 @@ +// Unit test for bzflag-0001: bz_EventHandler::HasEvent() O(N) vector scan +// replaced with O(1) bitset lookup. +// +// Defect: bz_EventHandler::HasEvent() used std::find on a std::vector +// to check if a handler handles a given event type. This is called inside +// WorldEventManager::callEvents() for every handler on every event fire, +// making it O(E * H) where E = handlers, H = events per handler. +// +// Fix: Replace std::vector HandledEvents with +// std::bitset handledEventBits. HasEvent/AddEvent/RemoveEvent +// become O(1) bitset operations. + +#include +#include +#include +#include +#include +#include + +// Minimal reproduction of BZFlag event types +enum bz_eEventType { + bz_eNullEvent = 0, + bz_eCaptureEvent, + bz_ePlayerDieEvent, + bz_ePlayerSpawnEvent, + bz_eZoneEntryEvent, + bz_eZoneExitEvent, + bz_ePlayerJoinEvent, + bz_ePlayerPartEvent, + bz_eRawChatMessageEvent, + bz_eFilteredChatMessageEvent, + bz_eUnknownSlashCommand, + bz_eGetPlayerSpawnPosEvent, + bz_eGetAutoTeamEvent, + bz_eAllowPlayer, + bz_eTickEvent, + bz_eGetWorldEvent, + bz_eGetPlayerInfoEvent, + bz_eAllowSpawn, + bz_eListServerUpdateEvent, + bz_eBanEvent, + bz_eHostBanModifyEvent, + bz_eKickEvent, + bz_eKillEvent, + bz_ePlayerPausedEvent, + bz_eMessageFilteredEvent, + bz_eGamePauseEvent, + bz_eGameResumeEvent, + bz_eGameStartEvent, + bz_eGameEndEvent, + bz_eSlashCommandEvent, + bz_ePlayerAuthEvent, + bz_eServerMsgEvent, + bz_eShotFiredEvent, + bz_ePlayerUpdateEvent, + bz_eNetDataSendEvent, + bz_eNetDataReceiveEvent, + bz_eLoggingEvent, + bz_eShotEndedEvent, + bz_eFlagTransferredEvent, + bz_eFlagGrabbedEvent, + bz_eFlagDroppedEvent, + bz_eAllowCTFCaptureEvent, + bz_eMsgDebugEvent, + bz_eNewNonPlayerConnection, + bz_ePluginLoaded, + bz_ePluginUnloaded, + bz_ePlayerScoreChanged, + bz_eTeamScoreChanged, + bz_eWorldFinalized, + bz_eReportFiledEvent, + bz_eBZDBChange, + bz_eGetPlayerMotto, + bz_eAllowConnection, + bz_eAllowFlagGrab, + bz_eAuthenticatonComplete, + bz_eServerAddPlayer, + bz_eAllowPollEvent, + bz_ePollStartEvent, + bz_ePollVoteEvent, + bz_ePollVetoEvent, + bz_ePollEndEvent, + bz_eComputeHandicapEvent, + bz_eBeginHandicapRefreshEvent, + bz_eEndHandicapRefreshEvent, + bz_eAutoPilotEvent, + bz_eMuteEvent, + bz_eUnmuteEvent, + bz_eServerShotFiredEvent, + bz_ePermissionModificationEvent, + bz_eAllowServerShotFiredEvent, + bz_ePlayerDeathFinalizedEvent, + bz_eLastEvent +}; + +// BEFORE: vector-based HasEvent (original defective code) +struct EventHandlerBefore { + std::vector HandledEvents; + + bool HasEvent(bz_eEventType evt) { + return std::find(HandledEvents.begin(), HandledEvents.end(), evt) != HandledEvents.end(); + } + + void AddEvent(bz_eEventType evt) { + if (std::find(HandledEvents.begin(), HandledEvents.end(), evt) == HandledEvents.end()) + HandledEvents.push_back(evt); + } + + void RemoveEvent(bz_eEventType evt) { + auto itr = std::find(HandledEvents.begin(), HandledEvents.end(), evt); + if (itr != HandledEvents.end()) + HandledEvents.erase(itr); + } + + bool IsEmpty() const { return HandledEvents.empty(); } +}; + +// AFTER: bitset-based HasEvent (patched code) +struct EventHandlerAfter { + std::bitset handledEventBits; + + bool HasEvent(bz_eEventType evt) { + return (evt >= 0 && evt < bz_eLastEvent) && handledEventBits.test(evt); + } + + void AddEvent(bz_eEventType evt) { + if (evt >= 0 && evt < bz_eLastEvent) + handledEventBits.set(evt); + } + + void RemoveEvent(bz_eEventType evt) { + if (evt >= 0 && evt < bz_eLastEvent) + handledEventBits.reset(evt); + } + + bool HasNoEvents() const { return handledEventBits.none(); } +}; + +// Test correctness +void test_correctness() { + EventHandlerAfter h; + + // Initially no events + assert(!h.HasEvent(bz_eTickEvent)); + assert(!h.HasEvent(bz_eShotFiredEvent)); + assert(h.HasNoEvents()); + + // Add events + h.AddEvent(bz_eTickEvent); + h.AddEvent(bz_eShotFiredEvent); + h.AddEvent(bz_ePlayerUpdateEvent); + assert(h.HasEvent(bz_eTickEvent)); + assert(h.HasEvent(bz_eShotFiredEvent)); + assert(h.HasEvent(bz_ePlayerUpdateEvent)); + assert(!h.HasEvent(bz_eCaptureEvent)); + assert(!h.HasNoEvents()); + + // Idempotent add + h.AddEvent(bz_eTickEvent); + assert(h.HasEvent(bz_eTickEvent)); + + // Remove + h.RemoveEvent(bz_eTickEvent); + assert(!h.HasEvent(bz_eTickEvent)); + assert(h.HasEvent(bz_eShotFiredEvent)); + + // Remove all + h.RemoveEvent(bz_eShotFiredEvent); + h.RemoveEvent(bz_ePlayerUpdateEvent); + assert(h.HasNoEvents()); + + // Boundary: first and last valid events + h.AddEvent(bz_eNullEvent); + assert(h.HasEvent(bz_eNullEvent)); + h.AddEvent((bz_eEventType)(bz_eLastEvent - 1)); + assert(h.HasEvent((bz_eEventType)(bz_eLastEvent - 1))); + + printf("PASS: correctness\n"); +} + +// Benchmark: simulate callEvents hot path +void test_performance() { + const int NUM_HANDLERS = 20; // typical plugin count + const int EVENTS_PER_HANDLER = 15; // events each handler registers for + const int ITERATIONS = 1000000; // event fires to simulate + + // Prepare event types each handler cares about + std::vector eventTypes; + for (int i = 0; i < EVENTS_PER_HANDLER && i < bz_eLastEvent; i++) + eventTypes.push_back((bz_eEventType)(i * 3 % bz_eLastEvent)); + + // Setup BEFORE handlers + std::vector beforeHandlers(NUM_HANDLERS); + for (auto &h : beforeHandlers) + for (auto evt : eventTypes) + h.AddEvent(evt); + + // Setup AFTER handlers + std::vector afterHandlers(NUM_HANDLERS); + for (auto &h : afterHandlers) + for (auto evt : eventTypes) + h.AddEvent(evt); + + // Benchmark BEFORE: simulate callEvents checking HasEvent for each handler + volatile int sink = 0; + auto t0 = std::chrono::high_resolution_clock::now(); + for (int iter = 0; iter < ITERATIONS; iter++) { + bz_eEventType queryEvt = (bz_eEventType)(iter % bz_eLastEvent); + for (int h = 0; h < NUM_HANDLERS; h++) { + if (beforeHandlers[h].HasEvent(queryEvt)) + sink++; + } + } + auto t1 = std::chrono::high_resolution_clock::now(); + double before_ms = std::chrono::duration(t1 - t0).count(); + + // Benchmark AFTER: same with bitset + volatile int sink2 = 0; + auto t2 = std::chrono::high_resolution_clock::now(); + for (int iter = 0; iter < ITERATIONS; iter++) { + bz_eEventType queryEvt = (bz_eEventType)(iter % bz_eLastEvent); + for (int h = 0; h < NUM_HANDLERS; h++) { + if (afterHandlers[h].HasEvent(queryEvt)) + sink2++; + } + } + auto t3 = std::chrono::high_resolution_clock::now(); + double after_ms = std::chrono::duration(t3 - t2).count(); + + double ratio = before_ms / after_ms; + printf("BEFORE: %.1f ms\n", before_ms); + printf("AFTER: %.1f ms\n", after_ms); + printf("Ratio: %.1fx speedup\n", ratio); + + // Patched version must be faster + assert(ratio > 2.0 && "Expected at least 2x speedup from bitset vs vector find"); + printf("PASS: performance (%.1fx)\n", ratio); +} + +int main() { + test_correctness(); + test_performance(); + printf("ALL TESTS PASSED\n"); + return 0; +} diff --git a/defects/bzflag-0002/patch/bzflag-0002.patch b/defects/bzflag-0002/patch/bzflag-0002.patch new file mode 100644 index 000000000..643c9da2e --- /dev/null +++ b/defects/bzflag-0002/patch/bzflag-0002.patch @@ -0,0 +1,104 @@ +--- a/src/bzfs/AccessControlList.h ++++ b/src/bzfs/AccessControlList.h +@@ -12,6 +12,7 @@ + + // System headers + #include ++#include + #include + #include + +@@ -173,6 +174,15 @@ + /* FIXME the AccessControlList assumes that 255 is a wildcard. it "should" + * include a cidr mask with each address. it's still useful as is, though + * see wildcard conversion occurs in convert(). ++ * ++ * PERF NOTE: ban(), hostBan(), and idBan() each use std::find() on our ++ * vector to detect duplicates before inserting. When called from merge() ++ * which processes every entry in a master ban list, this makes ban list ++ * loading O(B^2) where B = number of bans. For large master ban lists ++ * (hundreds to thousands of entries from community servers) this is ++ * significant. Fix: maintain a parallel unordered_set index keyed on ++ * the ban identity (IP+CIDR for IP bans, hostpat for host bans, idpat ++ * for ID bans) to achieve O(1) duplicate detection. + */ + + /** This class handles the lists of bans and hostbans. It has functions for +@@ -296,6 +306,15 @@ + typedef std::vector idBanList_t; + idBanList_t idBanList; + ++ // O(1) duplicate detection indexes. ++ // Key for IP bans: (addr.s_addr, cidr) packed as uint64. ++ // Key for host bans: hostpat string. ++ // Key for ID bans: idpat string. ++ std::unordered_set banIndex; ++ std::unordered_set hostBanIndex; ++ std::unordered_set idBanIndex; ++ ++ static uint64_t banKey(in_addr addr, unsigned char cidr) { return ((uint64_t)addr.s_addr << 8) | cidr; } + std::string banFile; + + private: +--- a/src/bzfs/AccessControlList.cxx ++++ b/src/bzfs/AccessControlList.cxx +@@ -41,9 +41,12 @@ + { + BanInfo toban(ipAddr, bannedBy, period, cidr, fromMaster); + if (reason) toban.reason = reason; +- banList_t::iterator oldit = std::find(banList.begin(), banList.end(), toban); +- if (oldit != banList.end()) // IP already in list? -> replace +- *oldit = toban; ++ uint64_t key = banKey(ipAddr, cidr); ++ if (banIndex.count(key)) { ++ // IP already in list -> replace ++ banList_t::iterator oldit = std::find(banList.begin(), banList.end(), toban); ++ if (oldit != banList.end()) ++ *oldit = toban; ++ } + else ++ { ++ banIndex.insert(key); + banList.push_back(toban); ++ } + } + + +@@ -93,9 +96,12 @@ + { + HostBanInfo toban(hostpat, bannedBy, period,fromMaster); + if (reason) toban.reason = reason; +- hostBanList_t::iterator oldit = std::find(hostBanList.begin(), hostBanList.end(), toban); +- if (oldit != hostBanList.end()) +- *oldit = toban; ++ if (hostBanIndex.count(hostpat)) { ++ hostBanList_t::iterator oldit = std::find(hostBanList.begin(), hostBanList.end(), toban); ++ if (oldit != hostBanList.end()) ++ *oldit = toban; ++ } + else ++ { ++ hostBanIndex.insert(hostpat); + hostBanList.push_back(toban); ++ } + } + + +@@ -106,9 +112,12 @@ + { + IdBanInfo toban(idpat, bannedBy, period, fromMaster); + if (reason) toban.reason = reason; +- idBanList_t::iterator oldit = std::find(idBanList.begin(), idBanList.end(), toban); +- if (oldit != idBanList.end()) +- *oldit = toban; ++ if (idBanIndex.count(idpat)) { ++ idBanList_t::iterator oldit = std::find(idBanList.begin(), idBanList.end(), toban); ++ if (oldit != idBanList.end()) ++ *oldit = toban; ++ } + else ++ { ++ idBanIndex.insert(idpat); + idBanList.push_back(toban); ++ } + } diff --git a/defects/bzflag-0002/test/test b/defects/bzflag-0002/test/test new file mode 100755 index 000000000..4a50eb64d Binary files /dev/null and b/defects/bzflag-0002/test/test differ diff --git a/defects/bzflag-0002/test/test_ban_list_dedup.cpp b/defects/bzflag-0002/test/test_ban_list_dedup.cpp new file mode 100644 index 000000000..847153fd1 --- /dev/null +++ b/defects/bzflag-0002/test/test_ban_list_dedup.cpp @@ -0,0 +1,143 @@ +// Unit test for bzflag-0002: AccessControlList ban/hostBan/idBan O(B^2) dedup +// +// Defect: ban(), hostBan(), and idBan() each use std::find() on a +// std::vector to detect duplicate bans before inserting. When called from +// merge() processing a master ban list, this makes ban list loading O(B^2). +// +// Fix: Maintain a parallel std::unordered_set index for O(1) duplicate +// detection. Only fall through to std::find for replacement when our +// index confirms a duplicate exists. + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Minimal reproduction of BZFlag ban structures +struct in_addr_sim { + uint32_t s_addr; +}; + +struct BanInfo { + in_addr_sim addr; + unsigned char cidr; + std::string bannedBy; + std::string reason; + + BanInfo(in_addr_sim a, unsigned char c) : addr(a), cidr(c) {} + + bool operator==(const BanInfo &rhs) const { + return addr.s_addr == rhs.addr.s_addr && cidr == rhs.cidr; + } +}; + +// BEFORE: original O(B^2) pattern +struct BanListBefore { + std::vector banList; + + void ban(in_addr_sim addr, unsigned char cidr) { + BanInfo toban(addr, cidr); + auto oldit = std::find(banList.begin(), banList.end(), toban); + if (oldit != banList.end()) + *oldit = toban; + else + banList.push_back(toban); + } +}; + +// AFTER: O(1) dedup with unordered_set index +struct BanListAfter { + std::vector banList; + std::unordered_set banIndex; + + static uint64_t banKey(in_addr_sim addr, unsigned char cidr) { + return ((uint64_t)addr.s_addr << 8) | cidr; + } + + void ban(in_addr_sim addr, unsigned char cidr) { + BanInfo toban(addr, cidr); + uint64_t key = banKey(addr, cidr); + if (banIndex.count(key)) { + auto oldit = std::find(banList.begin(), banList.end(), toban); + if (oldit != banList.end()) + *oldit = toban; + } else { + banIndex.insert(key); + banList.push_back(toban); + } + } +}; + +void test_correctness() { + BanListAfter acl; + + // Add unique bans + for (int i = 0; i < 100; i++) { + in_addr_sim a; + a.s_addr = (uint32_t)i; + acl.ban(a, 32); + } + assert(acl.banList.size() == 100); + + // Add duplicate: should replace, not add + in_addr_sim dup; + dup.s_addr = 50; + acl.ban(dup, 32); + assert(acl.banList.size() == 100); + + // Different CIDR = different ban + dup.s_addr = 50; + acl.ban(dup, 24); + assert(acl.banList.size() == 101); + + printf("PASS: correctness\n"); +} + +void test_performance() { + const int NUM_BANS = 5000; + + // Benchmark BEFORE + BanListBefore before; + auto t0 = std::chrono::high_resolution_clock::now(); + for (int i = 0; i < NUM_BANS; i++) { + in_addr_sim a; + a.s_addr = (uint32_t)i; + before.ban(a, 32); + } + auto t1 = std::chrono::high_resolution_clock::now(); + double before_ms = std::chrono::duration(t1 - t0).count(); + + // Benchmark AFTER + BanListAfter after; + auto t2 = std::chrono::high_resolution_clock::now(); + for (int i = 0; i < NUM_BANS; i++) { + in_addr_sim a; + a.s_addr = (uint32_t)i; + after.ban(a, 32); + } + auto t3 = std::chrono::high_resolution_clock::now(); + double after_ms = std::chrono::duration(t3 - t2).count(); + + assert(before.banList.size() == (size_t)NUM_BANS); + assert(after.banList.size() == (size_t)NUM_BANS); + + double ratio = before_ms / after_ms; + printf("BEFORE: %.1f ms (%d bans)\n", before_ms, NUM_BANS); + printf("AFTER: %.1f ms (%d bans)\n", after_ms, NUM_BANS); + printf("Ratio: %.1fx speedup\n", ratio); + + assert(ratio > 5.0 && "Expected at least 5x speedup from hash index"); + printf("PASS: performance (%.1fx)\n", ratio); +} + +int main() { + test_correctness(); + test_performance(); + printf("ALL TESTS PASSED\n"); + return 0; +} diff --git a/defects/bzflag-0003/patch/bzflag-0003.patch b/defects/bzflag-0003/patch/bzflag-0003.patch new file mode 100644 index 000000000..a7ba89910 --- /dev/null +++ b/defects/bzflag-0003/patch/bzflag-0003.patch @@ -0,0 +1,26 @@ +--- a/src/bzfs/Permissions.cxx ++++ b/src/bzfs/Permissions.cxx +@@ -536,6 +536,7 @@ + // return value is only needed for groupdb parsing, not for userdb. + bool parsePermissionString(const std::string &permissionString, PlayerAccessInfo &info) + { ++ std::set customPermsSeen; + if (permissionString.length() < 1) + return false; + +@@ -649,10 +650,14 @@ + { + // Easy access + std::vector& c = info.customPerms; ++ // Populate seen set on first custom perm encounter ++ if (customPermsSeen.empty() && !c.empty()) ++ customPermsSeen.insert(c.begin(), c.end()); + + // Only store the custom permission if it doesn't exist, in order to prevent duplicates +- if (std::find(c.begin(), c.end(), word) == c.end()) ++ if (customPermsSeen.find(word) == customPermsSeen.end()) + { ++ customPermsSeen.insert(word); + c.push_back(word); + } + } diff --git a/defects/bzflag-0003/test/test b/defects/bzflag-0003/test/test new file mode 100755 index 000000000..e6e38942f Binary files /dev/null and b/defects/bzflag-0003/test/test differ diff --git a/defects/bzflag-0003/test/test_parse_perm_dedup.cpp b/defects/bzflag-0003/test/test_parse_perm_dedup.cpp new file mode 100644 index 000000000..605882adf --- /dev/null +++ b/defects/bzflag-0003/test/test_parse_perm_dedup.cpp @@ -0,0 +1,108 @@ +// Unit test for bzflag-0003: parsePermissionString customPerms dedup O(W*C) +// +// Defect: parsePermissionString uses std::find on customPerms vector to +// check for duplicates when adding custom permissions. When a group file +// has many custom permissions, this is O(W*C) per parse call. +// +// Fix: Use a std::set shadow for O(log C) dedup lookups. + +#include +#include +#include +#include +#include +#include +#include + +// BEFORE: vector find dedup +struct PermInfoBefore { + std::vector customPerms; + + void addCustomPerm(const std::string &word) { + if (std::find(customPerms.begin(), customPerms.end(), word) == customPerms.end()) + customPerms.push_back(word); + } +}; + +// AFTER: set-backed dedup +struct PermInfoAfter { + std::vector customPerms; + std::set customPermsSeen; + + void addCustomPerm(const std::string &word) { + if (customPermsSeen.find(word) == customPermsSeen.end()) { + customPermsSeen.insert(word); + customPerms.push_back(word); + } + } +}; + +void test_correctness() { + PermInfoAfter info; + + info.addCustomPerm("CUSTOM_KICK"); + info.addCustomPerm("CUSTOM_BAN"); + info.addCustomPerm("CUSTOM_MUTE"); + assert(info.customPerms.size() == 3); + + // Duplicate should not be added + info.addCustomPerm("CUSTOM_KICK"); + assert(info.customPerms.size() == 3); + + // Order preserved + assert(info.customPerms[0] == "CUSTOM_KICK"); + assert(info.customPerms[1] == "CUSTOM_BAN"); + assert(info.customPerms[2] == "CUSTOM_MUTE"); + + printf("PASS: correctness\n"); +} + +void test_performance() { + const int NUM_PERMS = 2000; + + // Generate unique perm names + std::vector perms; + for (int i = 0; i < NUM_PERMS; i++) + perms.push_back("CUSTOM_PERM_" + std::to_string(i)); + + // Benchmark BEFORE + PermInfoBefore before; + auto t0 = std::chrono::high_resolution_clock::now(); + for (int round = 0; round < 10; round++) { + before.customPerms.clear(); + for (auto &p : perms) + before.addCustomPerm(p); + } + auto t1 = std::chrono::high_resolution_clock::now(); + double before_ms = std::chrono::duration(t1 - t0).count(); + + // Benchmark AFTER + PermInfoAfter after; + auto t2 = std::chrono::high_resolution_clock::now(); + for (int round = 0; round < 10; round++) { + after.customPerms.clear(); + after.customPermsSeen.clear(); + for (auto &p : perms) + after.addCustomPerm(p); + } + auto t3 = std::chrono::high_resolution_clock::now(); + double after_ms = std::chrono::duration(t3 - t2).count(); + + assert(before.customPerms.size() == (size_t)NUM_PERMS); + assert(after.customPerms.size() == (size_t)NUM_PERMS); + + double ratio = before_ms / after_ms; + printf("BEFORE: %.1f ms (%d perms)\n", before_ms, NUM_PERMS); + printf("AFTER: %.1f ms (%d perms)\n", after_ms, NUM_PERMS); + printf("Ratio: %.1fx speedup\n", ratio); + + assert(ratio > 3.0 && "Expected at least 3x speedup from set dedup"); + printf("PASS: performance (%.1fx)\n", ratio); +} + +int main() { + test_correctness(); + test_performance(); + printf("ALL TESTS PASSED\n"); + return 0; +}