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.
157 lines
5.9 KiB
Java
157 lines
5.9 KiB
Java
import java.util.*;
|
|
|
|
/**
|
|
* Unit test simulating Forgejo forgejo-0002:
|
|
* modules/repository/init.go LoadRepoConfig() license sort uses O(P*L) scans:
|
|
*
|
|
* for _, name := range setting.Repository.PreferredLicenses {
|
|
* if util.SliceContainsString(Licenses, name, true) { // O(L) scan
|
|
* sortedLicenses = append(sortedLicenses, name)
|
|
* }
|
|
* }
|
|
* for _, name := range Licenses {
|
|
* if !util.SliceContainsString(setting.Repository.PreferredLicenses, name, true) { // O(P) scan
|
|
* sortedLicenses = append(sortedLicenses, name)
|
|
* }
|
|
* }
|
|
*
|
|
* Forgejo ships 776 licenses. Any preferred list of P entries costs O(P*776 + 776*P).
|
|
* Fix: build sets for O(1) lookup before the loops.
|
|
*/
|
|
public class ForgejoLicenseSortTest {
|
|
|
|
// Defective: O(P*L + L*P)
|
|
static List<String> sortLicensesDefective(
|
|
List<String> allLicenses, List<String> preferred) {
|
|
List<String> sorted = new ArrayList<>(allLicenses.size());
|
|
for (String name : preferred) {
|
|
if (containsIgnoreCase(allLicenses, name)) {
|
|
sorted.add(name);
|
|
}
|
|
}
|
|
for (String name : allLicenses) {
|
|
if (!containsIgnoreCase(preferred, name)) {
|
|
sorted.add(name);
|
|
}
|
|
}
|
|
return sorted;
|
|
}
|
|
|
|
static boolean containsIgnoreCase(List<String> list, String target) {
|
|
String t = target.toLowerCase(Locale.ROOT);
|
|
for (String s : list) {
|
|
if (s.toLowerCase(Locale.ROOT).equals(t)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Fixed: build sets first, then O(P + L)
|
|
static List<String> sortLicensesFixed(
|
|
List<String> allLicenses, List<String> preferred) {
|
|
Set<String> licenseSet = new HashSet<>(allLicenses.size() * 2);
|
|
for (String s : allLicenses) licenseSet.add(s.toLowerCase(Locale.ROOT));
|
|
|
|
Set<String> preferredSet = new HashSet<>(preferred.size() * 2);
|
|
for (String s : preferred) preferredSet.add(s.toLowerCase(Locale.ROOT));
|
|
|
|
List<String> sorted = new ArrayList<>(allLicenses.size());
|
|
for (String name : preferred) {
|
|
if (licenseSet.contains(name.toLowerCase(Locale.ROOT))) {
|
|
sorted.add(name);
|
|
}
|
|
}
|
|
for (String name : allLicenses) {
|
|
if (!preferredSet.contains(name.toLowerCase(Locale.ROOT))) {
|
|
sorted.add(name);
|
|
}
|
|
}
|
|
return sorted;
|
|
}
|
|
|
|
static long countDefectiveOps(List<String> allLicenses, List<String> preferred) {
|
|
long ops = 0;
|
|
// First loop: P * L
|
|
for (String name : preferred) {
|
|
ops += allLicenses.size();
|
|
}
|
|
// Second loop: L * P
|
|
for (String name : allLicenses) {
|
|
ops += preferred.size();
|
|
}
|
|
return ops;
|
|
}
|
|
|
|
static long countFixedOps(List<String> allLicenses, List<String> preferred) {
|
|
// Build sets: O(L + P)
|
|
long ops = allLicenses.size() + preferred.size();
|
|
// Two loops: O(P + L)
|
|
ops += preferred.size() + allLicenses.size();
|
|
return ops;
|
|
}
|
|
|
|
// Build a realistic license list like Forgejo's 776 licenses
|
|
static List<String> makeLicenses(int count) {
|
|
List<String> licenses = new ArrayList<>(count);
|
|
String[] templates = {
|
|
"MIT", "Apache-2.0", "GPL-2.0", "GPL-3.0", "LGPL-2.1",
|
|
"BSD-2-Clause", "BSD-3-Clause", "ISC", "MPL-2.0", "CDDL-1.0"
|
|
};
|
|
for (int i = 0; i < count; i++) {
|
|
licenses.add(templates[i % templates.length] + "-variant-" + i);
|
|
}
|
|
return licenses;
|
|
}
|
|
|
|
public static void main(String[] args) {
|
|
System.out.println("forgejo-0002: LoadRepoConfig license sort O(P*L) -> O(P+L)");
|
|
System.out.println("=".repeat(60));
|
|
|
|
// Correctness test with small data
|
|
List<String> licenses = Arrays.asList(
|
|
"MIT", "Apache-2.0", "GPL-2.0", "GPL-3.0", "BSD-2-Clause"
|
|
);
|
|
List<String> preferred = Arrays.asList("GPL-3.0", "MIT");
|
|
|
|
List<String> defective = sortLicensesDefective(licenses, preferred);
|
|
List<String> fixed = sortLicensesFixed(licenses, preferred);
|
|
|
|
// Both should start with preferred items (in preferred order), rest follow
|
|
if (!defective.get(0).equals("GPL-3.0") || !defective.get(1).equals("MIT")) {
|
|
throw new AssertionError("Defective sort wrong ordering: " + defective);
|
|
}
|
|
if (!fixed.get(0).equals("GPL-3.0") || !fixed.get(1).equals("MIT")) {
|
|
throw new AssertionError("Fixed sort wrong ordering: " + fixed);
|
|
}
|
|
if (defective.size() != fixed.size()) {
|
|
throw new AssertionError("Size mismatch: " + defective.size() + " vs " + fixed.size());
|
|
}
|
|
System.out.println("PASS: correctness verified");
|
|
|
|
// Benchmark: 776 licenses (realistic), various preferred counts
|
|
System.out.printf("%n%-10s %15s %12s %10s%n",
|
|
"licenses", "defective_ops", "fixed_ops", "ratio");
|
|
System.out.println("-".repeat(48));
|
|
|
|
int L = 776;
|
|
List<String> allLicenses = makeLicenses(L);
|
|
int[] prefCounts = {1, 5, 10, 20};
|
|
for (int P : prefCounts) {
|
|
List<String> pref = allLicenses.subList(0, P);
|
|
long def = countDefectiveOps(allLicenses, pref);
|
|
long fix = countFixedOps(allLicenses, pref);
|
|
double ratio = (double) def / fix;
|
|
System.out.printf("L=%-4d P=%-4d %15d %12d %9.1fx%n", L, P, def, fix, ratio);
|
|
}
|
|
|
|
long def776 = countDefectiveOps(allLicenses, allLicenses.subList(0, 5));
|
|
long fix776 = countFixedOps(allLicenses, allLicenses.subList(0, 5));
|
|
double ratio776 = (double) def776 / fix776;
|
|
|
|
if (ratio776 < 4) {
|
|
throw new AssertionError("Expected >4x ratio at L=776, P=5, got " + ratio776);
|
|
}
|
|
|
|
System.out.println("\nAll assertions PASS");
|
|
System.out.println("Fix: build licensesSet and preferredSet before loops for O(1) lookup");
|
|
}
|
|
}
|