langchain-0001: MultiVectorRetriever._get_relevant_documents() dedup IDs from vectorstore sub_docs uses list.contains() inside loop, O(k^2). k is unbounded in production RAG pipelines (configurable via search_kwargs). Fix: track seen IDs in a set, keep list for order. 499.5x at k=1000. forgejo-0002: LoadRepoConfig() license sort O(P*L) where L=776 licenses. Two SliceContainsString calls in back-to-back loops iterate full license list for each preferred license and vice versa. Fix: build lookup sets before loops. 19.5x at P=20 preferred licenses. forgejo-0003: synchronizePublicKeys() three O(N*M) scans per LDAP sync. Dedup of providedKeys is O(K^2), plus two O(P*G) set-difference loops. Runs per user per sync cycle. Fix: use maps for O(1) membership. 178.6x at K=G=500. forgejo-0001 (search.go RepoIDs) already patched in prior scan. MOADs 0002-0005 CLEAN for both targets.
179 lines
6.7 KiB
Java
179 lines
6.7 KiB
Java
import java.util.*;
|
|
|
|
/**
|
|
* Unit test simulating Forgejo forgejo-0003:
|
|
* models/asymkey/ssh_key.go synchronizePublicKeys() has three O(N*M) scans:
|
|
*
|
|
* 1. Building providedKeys with dedup: O(K^2) where K = LDAP-provided key count
|
|
* for _, v := range sshPublicKeys {
|
|
* key = ...
|
|
* if !util.SliceContainsString(providedKeys, key) { // O(|providedKeys|) each time
|
|
*
|
|
* 2. Finding new keys: O(P * G) where P = provided, G = DB keys
|
|
* for _, key := range providedKeys {
|
|
* if !util.SliceContainsString(giteaKeys, key) { // O(G) each
|
|
*
|
|
* 3. Finding deleted keys: O(G * P)
|
|
* for _, giteaKey := range giteaKeys {
|
|
* if !util.SliceContainsString(providedKeys, giteaKey) { // O(P) each
|
|
*
|
|
* This runs per user per LDAP sync cycle. With many users or large key sets it accumulates.
|
|
* Fix: build maps for O(1) lookup before each loop.
|
|
*/
|
|
public class ForgejoSSHKeySyncTest {
|
|
|
|
// Defective: O(K^2) dedup + O(P*G) + O(G*P)
|
|
static long countDefectiveOps(int rawKeyCount, int giteaKeyCount) {
|
|
// Phase 1: dedup raw keys into providedKeys
|
|
// Assume half are unique: K/2 unique keys, K/2 duplicates
|
|
// Each insertion checks against growing list
|
|
long ops = 0;
|
|
int provided = 0;
|
|
for (int i = 0; i < rawKeyCount; i++) {
|
|
ops += provided; // SliceContainsString cost
|
|
if (i % 2 == 0) provided++; // ~half unique
|
|
}
|
|
// Phase 2: find new keys (provided not in gitea)
|
|
ops += (long) provided * giteaKeyCount;
|
|
// Phase 3: find keys to delete (gitea not in provided)
|
|
ops += (long) giteaKeyCount * provided;
|
|
return ops;
|
|
}
|
|
|
|
static long countFixedOps(int rawKeyCount, int giteaKeyCount) {
|
|
// Phase 1: dedup with set — O(K) total
|
|
long ops = rawKeyCount;
|
|
int provided = rawKeyCount / 2; // ~half unique
|
|
// Build giteaKeysSet: O(G)
|
|
ops += giteaKeyCount;
|
|
// Phase 2: find new keys — O(P)
|
|
ops += provided;
|
|
// Phase 3: find keys to delete — O(G)
|
|
ops += giteaKeyCount;
|
|
return ops;
|
|
}
|
|
|
|
// Simulate the sync logic for correctness
|
|
static Map<String, List<String>> syncKeysDefective(
|
|
List<String> rawKeys, List<String> giteaKeys) {
|
|
// Dedup rawKeys
|
|
List<String> provided = new ArrayList<>();
|
|
for (String k : rawKeys) {
|
|
if (!provided.contains(k)) {
|
|
provided.add(k);
|
|
}
|
|
}
|
|
// Find new keys
|
|
List<String> toAdd = new ArrayList<>();
|
|
for (String k : provided) {
|
|
if (!giteaKeys.contains(k)) {
|
|
toAdd.add(k);
|
|
}
|
|
}
|
|
// Find keys to delete
|
|
List<String> toDelete = new ArrayList<>();
|
|
for (String k : giteaKeys) {
|
|
if (!provided.contains(k)) {
|
|
toDelete.add(k);
|
|
}
|
|
}
|
|
Map<String, List<String>> result = new HashMap<>();
|
|
result.put("toAdd", toAdd);
|
|
result.put("toDelete", toDelete);
|
|
return result;
|
|
}
|
|
|
|
static Map<String, List<String>> syncKeysFixed(
|
|
List<String> rawKeys, List<String> giteaKeys) {
|
|
// Dedup rawKeys with set
|
|
Set<String> providedSet = new LinkedHashSet<>();
|
|
List<String> provided = new ArrayList<>();
|
|
for (String k : rawKeys) {
|
|
if (providedSet.add(k)) {
|
|
provided.add(k);
|
|
}
|
|
}
|
|
// Build giteaKeys set
|
|
Set<String> giteaSet = new HashSet<>(giteaKeys);
|
|
// Find new keys
|
|
List<String> toAdd = new ArrayList<>();
|
|
for (String k : provided) {
|
|
if (!giteaSet.contains(k)) {
|
|
toAdd.add(k);
|
|
}
|
|
}
|
|
// Find keys to delete
|
|
List<String> toDelete = new ArrayList<>();
|
|
for (String k : giteaKeys) {
|
|
if (!providedSet.contains(k)) {
|
|
toDelete.add(k);
|
|
}
|
|
}
|
|
Map<String, List<String>> result = new HashMap<>();
|
|
result.put("toAdd", toAdd);
|
|
result.put("toDelete", toDelete);
|
|
return result;
|
|
}
|
|
|
|
public static void main(String[] args) {
|
|
System.out.println("forgejo-0003: synchronizePublicKeys() O(K^2+P*G) -> O(K+P+G)");
|
|
System.out.println("=".repeat(60));
|
|
|
|
// Correctness test
|
|
List<String> rawKeys = Arrays.asList(
|
|
"ssh-rsa AAAA1", "ssh-rsa AAAA2", "ssh-rsa AAAA1", // dup
|
|
"ssh-rsa AAAA3", "ssh-rsa AAAA2" // more dups
|
|
);
|
|
List<String> giteaKeys = Arrays.asList(
|
|
"ssh-rsa AAAA2", "ssh-rsa AAAA4"
|
|
);
|
|
|
|
Map<String, List<String>> def = syncKeysDefective(rawKeys, giteaKeys);
|
|
Map<String, List<String>> fix = syncKeysFixed(rawKeys, giteaKeys);
|
|
|
|
// Expected: add AAAA1 and AAAA3 (new from LDAP), delete AAAA4 (no longer in LDAP)
|
|
List<String> expectedAdd = Arrays.asList("ssh-rsa AAAA1", "ssh-rsa AAAA3");
|
|
List<String> expectedDelete = Arrays.asList("ssh-rsa AAAA4");
|
|
|
|
if (!def.get("toAdd").equals(expectedAdd)) {
|
|
throw new AssertionError("Defective toAdd wrong: " + def.get("toAdd"));
|
|
}
|
|
if (!def.get("toDelete").equals(expectedDelete)) {
|
|
throw new AssertionError("Defective toDelete wrong: " + def.get("toDelete"));
|
|
}
|
|
if (!fix.get("toAdd").equals(expectedAdd)) {
|
|
throw new AssertionError("Fixed toAdd wrong: " + fix.get("toAdd"));
|
|
}
|
|
if (!fix.get("toDelete").equals(expectedDelete)) {
|
|
throw new AssertionError("Fixed toDelete wrong: " + fix.get("toDelete"));
|
|
}
|
|
System.out.println("PASS: correctness verified");
|
|
|
|
// Performance comparison
|
|
System.out.printf("%n%-20s %15s %12s %10s%n",
|
|
"scenario", "defective_ops", "fixed_ops", "ratio");
|
|
System.out.println("-".repeat(58));
|
|
|
|
int[][] scenarios = {
|
|
{10, 10}, {50, 50}, {100, 100}, {500, 500}, {1000, 1000}
|
|
};
|
|
for (int[] sc : scenarios) {
|
|
int K = sc[0], G = sc[1];
|
|
long dOps = countDefectiveOps(K, G);
|
|
long fOps = countFixedOps(K, G);
|
|
double ratio = (double) dOps / fOps;
|
|
System.out.printf("K=%-4d G=%-4d %15d %12d %9.1fx%n",
|
|
K, G, dOps, fOps, ratio);
|
|
}
|
|
|
|
long dOps = countDefectiveOps(500, 500);
|
|
long fOps = countFixedOps(500, 500);
|
|
double ratio = (double) dOps / fOps;
|
|
if (ratio < 10) {
|
|
throw new AssertionError("Expected >10x ratio at K=500 G=500, got " + ratio);
|
|
}
|
|
|
|
System.out.println("\nAll assertions PASS");
|
|
System.out.println("Fix: use providedKeysSet(map) for O(1) dedup; giteaKeysSet for O(1) diff");
|
|
}
|
|
}
|