diff --git a/defects/duckstation-0001/patch/duckstation-0001-cheats-unique-prefixes-set.patch b/defects/duckstation-0001/patch/duckstation-0001-cheats-unique-prefixes-set.patch new file mode 100644 index 000000000..fda509200 --- /dev/null +++ b/defects/duckstation-0001/patch/duckstation-0001-cheats-unique-prefixes-set.patch @@ -0,0 +1,25 @@ +# UNDF: UNDF-2026-XXXXXXXXX +--- a/src/core/cheats.cpp ++++ b/src/core/cheats.cpp +@@ -598,6 +598,8 @@ std::vector Cheats::GetCodeListUniquePrefixes(const CodeInfoLi + std::vector Cheats::GetCodeListUniquePrefixes(const CodeInfoList& list, bool include_empty) + { + std::vector ret; ++ // Use a hash set to test prefix membership in O(1) instead of O(N) std::find, ++ // eliminating O(N^2) behaviour when building our unique-prefix list. ++ std::unordered_set seen; + for (const Cheats::CodeInfo& code : list) + { + const std::string_view prefix = code.GetNameParentPart(); +@@ -608,7 +610,9 @@ std::vector Cheats::GetCodeListUniquePrefixes(const CodeInfoLi + continue; + } + +- if (std::find(ret.begin(), ret.end(), prefix) == ret.end()) ++ if (seen.insert(prefix).second) ++ { + ret.push_back(prefix); ++ } + } + return ret; + } diff --git a/defects/duckstation-0001/test/CheatsUniquePrefixesTest.class b/defects/duckstation-0001/test/CheatsUniquePrefixesTest.class new file mode 100644 index 000000000..12166f4c7 Binary files /dev/null and b/defects/duckstation-0001/test/CheatsUniquePrefixesTest.class differ diff --git a/defects/duckstation-0001/test/CheatsUniquePrefixesTest.java b/defects/duckstation-0001/test/CheatsUniquePrefixesTest.java new file mode 100644 index 000000000..19ad69209 --- /dev/null +++ b/defects/duckstation-0001/test/CheatsUniquePrefixesTest.java @@ -0,0 +1,92 @@ +import java.util.*; + +/** + * Unit test for DuckStation duckstation-0001: + * Cheats::GetCodeListUniquePrefixes uses std::find on a growing std::vector + * inside a loop over all cheat codes, giving O(N^2) behaviour. + * + * PS1 cheat databases (GameShark, CodeBreaker) contain 100s to 1000s of codes + * per game. This function is called every time our cheat settings UI tab opens. + * + * Defect file: src/core/cheats.cpp line 614 + * Pattern: for each code, std::find(ret.begin(), ret.end(), prefix) + * Fix: insert into unordered_set for O(1) membership test per code. + */ +public class CheatsUniquePrefixesTest { + + // --- Defective: O(N^2) linear scan dedup --- + static List getUniquePrefixesDefective(List codes) { + List ret = new ArrayList<>(); + for (String code : codes) { + // Simulate GetNameParentPart: prefix is everything before '/' + int slash = code.lastIndexOf('/'); + String prefix = (slash >= 0) ? code.substring(0, slash) : ""; + if (prefix.isEmpty()) continue; + if (!ret.contains(prefix)) { // O(N) scan per code + ret.add(prefix); + } + } + return ret; + } + + // --- Fixed: O(N) with hash set --- + static List getUniquePrefixesFixed(List codes) { + List ret = new ArrayList<>(); + Set seen = new HashSet<>(); + for (String code : codes) { + int slash = code.lastIndexOf('/'); + String prefix = (slash >= 0) ? code.substring(0, slash) : ""; + if (prefix.isEmpty()) continue; + if (seen.add(prefix)) { + ret.add(prefix); + } + } + return ret; + } + + public static void main(String[] args) { + int NUM_GROUPS = 200; + int CODES_PER_GROUP = 5; + int N = NUM_GROUPS * CODES_PER_GROUP; + + // Build test data: codes named "Group_N/Code_M" + List codes = new ArrayList<>(N); + for (int g = 0; g < NUM_GROUPS; g++) { + for (int c = 0; c < CODES_PER_GROUP; c++) { + codes.add("Group_" + g + "/Code_" + c); + } + } + + // Correctness check + List defResult = getUniquePrefixesDefective(codes); + List fixResult = getUniquePrefixesFixed(codes); + Collections.sort(defResult); + Collections.sort(fixResult); + assert defResult.equals(fixResult) + : "Prefix list mismatch: defective=" + defResult.size() + " fixed=" + fixResult.size(); + assert fixResult.size() == NUM_GROUPS : "Expected " + NUM_GROUPS + " unique prefixes"; + + // Warm up + for (int i = 0; i < 100; i++) { + getUniquePrefixesDefective(codes); + getUniquePrefixesFixed(codes); + } + + // Benchmark + int ITER = 1000; + long t0 = System.nanoTime(); + for (int i = 0; i < ITER; i++) getUniquePrefixesDefective(codes); + long defectNs = System.nanoTime() - t0; + + t0 = System.nanoTime(); + for (int i = 0; i < ITER; i++) getUniquePrefixesFixed(codes); + long fixedNs = System.nanoTime() - t0; + + double ratio = (double) defectNs / fixedNs; + System.out.printf("GetCodeListUniquePrefixes N=%d codes (%d groups) defect=%.1fms fixed=%.1fms ratio=%.1fx%n", + N, NUM_GROUPS, defectNs / 1e6, fixedNs / 1e6, ratio); + + assert ratio > 2.0 : "Expected >2x speedup, got " + ratio; + System.out.println("PASS"); + } +} diff --git a/defects/duckstation-0002/patch/duckstation-0002-cheats-enable-patches-set.patch b/defects/duckstation-0002/patch/duckstation-0002-cheats-enable-patches-set.patch new file mode 100644 index 000000000..4c1af6944 --- /dev/null +++ b/defects/duckstation-0002/patch/duckstation-0002-cheats-enable-patches-set.patch @@ -0,0 +1,22 @@ +# UNDF: UNDF-2026-XXXXXXXXX +--- a/src/core/cheats.cpp ++++ b/src/core/cheats.cpp +@@ -893,6 +893,8 @@ u32 Cheats::EnablePatches(const CheatCodeList& patches, const EnableCodeList& en + u32 Cheats::EnablePatches(const CheatCodeList& patches, const EnableCodeList& enable_list, const char* section, + bool hc_mode_active) + { ++ // Convert enable_list to a hash set for O(1) lookup instead of O(E) std::find ++ // per patch, eliminating O(P*E) behaviour when activating our patch/cheat lists. ++ const std::unordered_set enable_set(enable_list.begin(), enable_list.end()); + u32 count = 0; + for (const std::unique_ptr& p : patches) + { +@@ -905,7 +907,7 @@ u32 Cheats::EnablePatches(const CheatCodeList& patches, const EnableCodeList& en + if (p->GetMetadata().disallow_for_achievements && hc_mode_active) + continue; + +- if (std::find(enable_list.begin(), enable_list.end(), p->GetName()) == enable_list.end()) ++ if (!enable_set.count(p->GetName())) + continue; + + INFO_LOG("Enabled code from {}: {}", section, p->GetName()); diff --git a/defects/duckstation-0002/test/CheatsEnablePatchesTest.class b/defects/duckstation-0002/test/CheatsEnablePatchesTest.class new file mode 100644 index 000000000..d4f741dd5 Binary files /dev/null and b/defects/duckstation-0002/test/CheatsEnablePatchesTest.class differ diff --git a/defects/duckstation-0002/test/CheatsEnablePatchesTest.java b/defects/duckstation-0002/test/CheatsEnablePatchesTest.java new file mode 100644 index 000000000..ea98a9a65 --- /dev/null +++ b/defects/duckstation-0002/test/CheatsEnablePatchesTest.java @@ -0,0 +1,91 @@ +import java.util.*; + +/** + * Unit test for DuckStation duckstation-0002: + * Cheats::EnablePatches loops over all patch codes and calls std::find on + * enable_list (std::vector<std::string>) for each patch, giving O(P*E) + * behaviour where P = patch count and E = enabled names list length. + * + * PS1 GameShark/CodeBreaker patch databases can contain 100s of codes per game. + * Called once at game load for both patches and cheats subsections. + * + * Defect file: src/core/cheats.cpp line 909 + * Pattern: for each patch, std::find(enable_list.begin(), enable_list.end(), name) + * Fix: build unordered_set from enable_list once, use count() for O(1) lookup. + */ +public class CheatsEnablePatchesTest { + + // --- Defective: O(P*E) linear scan per patch --- + static int enablePatchesDefective(List patches, List enableList) { + int count = 0; + for (String name : patches) { + if (!name.isEmpty() && !enableList.contains(name)) { // O(E) scan per patch + continue; + } + count++; + } + return count; + } + + // --- Fixed: O(P+E) with hash set --- + static int enablePatchesFixed(List patches, List enableList) { + Set enableSet = new HashSet<>(enableList); + int count = 0; + for (String name : patches) { + if (!name.isEmpty() && !enableSet.contains(name)) { // O(1) per patch + continue; + } + count++; + } + return count; + } + + public static void main(String[] args) { + int N = 500; + + List patches = new ArrayList<>(N); + List enableList = new ArrayList<>(N); + + for (int i = 0; i < N; i++) { + patches.add("Patch_" + i); + enableList.add("Patch_" + i); + } + + // Correctness check + int defCount = enablePatchesDefective(patches, enableList); + int fixCount = enablePatchesFixed(patches, enableList); + assert defCount == fixCount + : "Count mismatch: defective=" + defCount + " fixed=" + fixCount; + assert fixCount == N : "Expected all " + N + " patches enabled"; + + // Partial enable list + List partialEnable = enableList.subList(0, N / 2); + int defPartial = enablePatchesDefective(patches, partialEnable); + int fixPartial = enablePatchesFixed(patches, partialEnable); + assert defPartial == fixPartial + : "Partial count mismatch: defective=" + defPartial + " fixed=" + fixPartial; + + // Warm up + for (int i = 0; i < 200; i++) { + enablePatchesDefective(patches, enableList); + enablePatchesFixed(patches, enableList); + } + + // Benchmark + int ITER = 2000; + long t0 = System.nanoTime(); + for (int i = 0; i < ITER; i++) enablePatchesDefective(patches, enableList); + long defectNs = System.nanoTime() - t0; + + t0 = System.nanoTime(); + for (int i = 0; i < ITER; i++) enablePatchesFixed(patches, enableList); + long fixedNs = System.nanoTime() - t0; + + double ratio = (double) defectNs / fixedNs; + System.out.printf("EnablePatches N=%d defect=%.1fms fixed=%.1fms ratio=%.1fx%n", + N, defectNs / 1e6, fixedNs / 1e6, ratio); + + assert ratio > 2.0 : "Expected >2x speedup, got " + ratio; + System.out.println("PASS"); + } +}