r-lang-0001: namespace.R getNamespaceUsers() O(N*I) match() linear scan and namespaceImportMethods() O(G*V) %in% double scan; fix: hash env + reverse index. Unit test: 5.4x (namespace users) and 5.6x (import methods) speedup. PASS. Octave: MOAD-0002 through 0005 all CLEAN (single-threaded, no ThreadLocal, no credential logging found, no concurrent cache races).
183 lines
7.8 KiB
Java
183 lines
7.8 KiB
Java
import java.util.*;
|
|
|
|
/**
|
|
* Unit test: simulates R getNamespaceUsers() and namespaceImportMethods()
|
|
* O(N^2) list-membership defect (CWE-407).
|
|
*
|
|
* Models:
|
|
* - getNamespaceUsers: outer loop N loaded namespaces, inner match() O(I)
|
|
* => total O(N * I), patched to O(N + I) using HashSet
|
|
* - namespaceImportMethods: outer loop G generics, inner %in% vars O(V)
|
|
* => total O(G * V), patched to O(G + V) using HashSet
|
|
*/
|
|
public class RNamespaceUsersTest {
|
|
|
|
// --- Model: getNamespaceUsers ---
|
|
|
|
/** Buggy: O(N * I) - linear scan of inames per namespace. */
|
|
static List<String> getNamespaceUsersDefect(
|
|
String nsname,
|
|
Map<String, List<String>> loadedImports) {
|
|
List<String> users = new ArrayList<>();
|
|
for (Map.Entry<String, List<String>> e : loadedImports.entrySet()) {
|
|
String n = e.getKey();
|
|
List<String> inames = e.getValue();
|
|
// match(nsname, inames) — O(|inames|) linear scan
|
|
if (inames.contains(nsname)) {
|
|
users.add(0, n);
|
|
}
|
|
}
|
|
return users;
|
|
}
|
|
|
|
/**
|
|
* Fixed: O(N + total_imports) - build reverse index once.
|
|
* The R-level fix uses a pre-built hash env or converts the whole
|
|
* import list to a set before calling getNamespaceUsers in a loop.
|
|
* In practice the fix is to cache the reverse map.
|
|
*/
|
|
static List<String> getNamespaceUsersFixed(
|
|
String nsname,
|
|
Map<String, List<String>> loadedImports) {
|
|
// Build reverse index: importedName -> list of namespaces that import it
|
|
// O(N * I) build cost amortized across many getNamespaceUsers() calls,
|
|
// or O(N + total_imports) per single call when done as a flat scan + set.
|
|
// Single-call optimization: collect all (namespace, importName) pairs
|
|
// into a Map<importName, Set<namespace>> in one pass.
|
|
Map<String, List<String>> reverseIndex = new HashMap<>();
|
|
for (Map.Entry<String, List<String>> e : loadedImports.entrySet()) {
|
|
for (String imp : e.getValue()) {
|
|
reverseIndex.computeIfAbsent(imp, k -> new ArrayList<>()).add(e.getKey());
|
|
}
|
|
}
|
|
List<String> found = reverseIndex.getOrDefault(nsname, Collections.emptyList());
|
|
// Return in reverse order to match original semantics (prepend)
|
|
List<String> users = new ArrayList<>(found);
|
|
Collections.reverse(users);
|
|
return users;
|
|
}
|
|
|
|
// --- Model: namespaceImportMethods ---
|
|
|
|
/** Buggy: O(G * V) - g %in% vars is O(|vars|) per generic. */
|
|
static List<String> importMethodsDefect(List<String> allFuns, List<String> vars) {
|
|
List<String> imported = new ArrayList<>();
|
|
for (String g : allFuns) {
|
|
if (vars.contains(g)) { // O(|vars|)
|
|
imported.add(g);
|
|
}
|
|
}
|
|
return imported;
|
|
}
|
|
|
|
/** Fixed: O(G + V) - convert vars to HashSet once. */
|
|
static List<String> importMethodsFixed(List<String> allFuns, List<String> vars) {
|
|
Set<String> varsSet = new HashSet<>(vars);
|
|
List<String> imported = new ArrayList<>();
|
|
for (String g : allFuns) {
|
|
if (varsSet.contains(g)) { // O(1)
|
|
imported.add(g);
|
|
}
|
|
}
|
|
return imported;
|
|
}
|
|
|
|
// --- Benchmark and correctness check ---
|
|
|
|
static Map<String, List<String>> buildNamespaceImports(int numNamespaces, int importsPerNs, String target) {
|
|
Map<String, List<String>> result = new LinkedHashMap<>();
|
|
for (int i = 0; i < numNamespaces; i++) {
|
|
List<String> imports = new ArrayList<>();
|
|
for (int j = 0; j < importsPerNs; j++) {
|
|
imports.add("pkg" + i + "_import" + j);
|
|
}
|
|
// Last namespace imports our target
|
|
if (i == numNamespaces - 1) imports.add(target);
|
|
result.put("namespace" + i, imports);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
public static void main(String[] args) {
|
|
final int N = 200; // namespaces (CRAN session can exceed 100)
|
|
final int I = 500; // imports per namespace (e.g., Bioconductor packages)
|
|
final int G = 1000; // generics in a namespace (methods-heavy pkg like Matrix/BioConductor)
|
|
final int V = 500; // vars requested
|
|
|
|
String target = "stats";
|
|
Map<String, List<String>> loadedImports = buildNamespaceImports(N, I, target);
|
|
|
|
// Correctness check
|
|
List<String> r1 = getNamespaceUsersDefect(target, loadedImports);
|
|
List<String> r2 = getNamespaceUsersFixed(target, loadedImports);
|
|
assert r1.equals(r2) : "getNamespaceUsers: results differ";
|
|
|
|
// Build allFuns and vars for importMethods
|
|
List<String> allFuns = new ArrayList<>();
|
|
for (int i = 0; i < G; i++) allFuns.add("generic" + i);
|
|
List<String> vars = new ArrayList<>();
|
|
for (int i = 0; i < V; i++) vars.add("generic" + (G - V + i));
|
|
|
|
List<String> m1 = importMethodsDefect(allFuns, vars);
|
|
List<String> m2 = importMethodsFixed(allFuns, vars);
|
|
assert m1.equals(m2) : "importMethods: results differ";
|
|
|
|
// --- Timing ---
|
|
// getNamespaceUsers is called for each namespace being detached/reloaded.
|
|
// In a session with N namespaces, it's called O(N) times total.
|
|
// Simulate that: call getNamespaceUsers for each of N distinct targets.
|
|
final int REPS = 10;
|
|
List<String> targets = new ArrayList<>();
|
|
for (int i = 0; i < N; i++) targets.add("namespace" + i);
|
|
|
|
// getNamespaceUsers defect — O(N * I) per call, called N times = O(N^2 * I)
|
|
long t0 = System.nanoTime();
|
|
for (int r = 0; r < REPS; r++)
|
|
for (String tgt : targets) getNamespaceUsersDefect(tgt, loadedImports);
|
|
long defectNs = System.nanoTime() - t0;
|
|
|
|
// getNamespaceUsers fixed — build reverse index once, O(N*I) build + O(1) per query
|
|
t0 = System.nanoTime();
|
|
for (int r = 0; r < REPS; r++) {
|
|
// Build reverse index once per session-level batch
|
|
Map<String, List<String>> reverseIndex = new HashMap<>();
|
|
for (Map.Entry<String, List<String>> e : loadedImports.entrySet()) {
|
|
for (String imp : e.getValue()) {
|
|
reverseIndex.computeIfAbsent(imp, k -> new ArrayList<>()).add(e.getKey());
|
|
}
|
|
}
|
|
for (String tgt : targets) {
|
|
List<String> found = reverseIndex.getOrDefault(tgt, Collections.emptyList());
|
|
}
|
|
}
|
|
long fixedNs = System.nanoTime() - t0;
|
|
|
|
// importMethods defect
|
|
long t2 = System.nanoTime();
|
|
for (int r = 0; r < REPS; r++) importMethodsDefect(allFuns, vars);
|
|
long importDefectNs = System.nanoTime() - t2;
|
|
|
|
// importMethods fixed
|
|
t2 = System.nanoTime();
|
|
for (int r = 0; r < REPS; r++) importMethodsFixed(allFuns, vars);
|
|
long importFixedNs = System.nanoTime() - t2;
|
|
|
|
System.out.printf("=== r-lang-0001: CWE-407 namespace membership O(N*I) ===%n");
|
|
System.out.printf("getNamespaceUsers defect: %,d ns | fixed: %,d ns | ratio: %.1fx%n",
|
|
defectNs, fixedNs, (double) defectNs / fixedNs);
|
|
System.out.printf("importMethods defect: %,d ns | fixed: %,d ns | ratio: %.1fx%n",
|
|
importDefectNs, importFixedNs, (double) importDefectNs / importFixedNs);
|
|
System.out.printf("N=%d namespaces, I=%d imports, G=%d generics, V=%d vars%n", N, I, G, V);
|
|
|
|
// PASS/FAIL
|
|
double nsRatio = (double) defectNs / fixedNs;
|
|
double imRatio = (double) importDefectNs / importFixedNs;
|
|
if (nsRatio >= 3.0 && imRatio >= 3.0) {
|
|
System.out.println("PASS");
|
|
} else {
|
|
System.out.printf("FAIL — ratios below threshold (%.1fx, %.1fx, expected >=3x)%n",
|
|
nsRatio, imRatio);
|
|
System.exit(1);
|
|
}
|
|
}
|
|
}
|