227 lines
8.3 KiB
Java
227 lines
8.3 KiB
Java
package unit;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
import java.util.LinkedHashSet;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Objects;
|
|
|
|
/**
|
|
* Models cargo's Edges::add_edge() in src/cargo/ops/tree/graph.rs —
|
|
* deduplicates outgoing edges from a dependency graph node.
|
|
*
|
|
* SLOW: Vec<Edge>.contains() before each push — O(k) per insertion → O(E²) total.
|
|
* FAST: LinkedHashSet<Edge> (insertion-ordered, O(1) contains) — O(E) total.
|
|
*
|
|
* CWE-407: src/cargo/ops/tree/graph.rs:122-126
|
|
*/
|
|
public class EdgesAddEdgeAlgorithm {
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Edge model
|
|
// -------------------------------------------------------------------------
|
|
|
|
enum EdgeKind { DEP, FEATURE }
|
|
|
|
static final class Edge {
|
|
final EdgeKind kind;
|
|
final int node; // NodeId (simplified as int)
|
|
final boolean pub;
|
|
|
|
Edge(EdgeKind kind, int node, boolean pub) {
|
|
this.kind = kind;
|
|
this.node = node;
|
|
this.pub = pub;
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(Object o) {
|
|
if (!(o instanceof Edge)) return false;
|
|
Edge e = (Edge) o;
|
|
return kind == e.kind && node == e.node && pub == e.pub;
|
|
}
|
|
|
|
@Override
|
|
public int hashCode() {
|
|
return Objects.hash(kind, node, pub);
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Slow (defective) implementation — mirrors current cargo Vec<Edge>
|
|
// -------------------------------------------------------------------------
|
|
|
|
static class SlowEdges {
|
|
final Map<EdgeKind, List<Edge>> map = new HashMap<>();
|
|
long containsChecks = 0;
|
|
|
|
/** O(k) membership test before push — quadratic when k grows. */
|
|
void addEdge(Edge edge) {
|
|
List<Edge> bucket = map.computeIfAbsent(edge.kind, k -> new ArrayList<>());
|
|
containsChecks += bucket.size(); // count comparisons (worst case = full scan)
|
|
if (!bucket.contains(edge)) {
|
|
bucket.add(edge);
|
|
}
|
|
}
|
|
|
|
int edgeCount() {
|
|
return map.values().stream().mapToInt(List::size).sum();
|
|
}
|
|
|
|
List<Edge> ofKind(EdgeKind k) {
|
|
return map.getOrDefault(k, List.of());
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Fast (fixed) implementation — LinkedHashSet preserves insertion order
|
|
// -------------------------------------------------------------------------
|
|
|
|
static class FastEdges {
|
|
final Map<EdgeKind, LinkedHashSet<Edge>> map = new HashMap<>();
|
|
long insertCalls = 0;
|
|
|
|
/** O(1) amortized insert — LinkedHashSet ignores duplicates. */
|
|
void addEdge(Edge edge) {
|
|
LinkedHashSet<Edge> bucket = map.computeIfAbsent(edge.kind, k -> new LinkedHashSet<>());
|
|
insertCalls++;
|
|
bucket.add(edge);
|
|
}
|
|
|
|
int edgeCount() {
|
|
return map.values().stream().mapToInt(LinkedHashSet::size).sum();
|
|
}
|
|
|
|
List<Edge> ofKind(EdgeKind k) {
|
|
return new ArrayList<>(map.getOrDefault(k, new LinkedHashSet<>()));
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Test helpers
|
|
// -------------------------------------------------------------------------
|
|
|
|
static int passed = 0;
|
|
static int total = 0;
|
|
|
|
static void check(String desc, boolean cond) {
|
|
total++;
|
|
if (cond) {
|
|
passed++;
|
|
System.out.printf(" PASS %s%n", desc);
|
|
} else {
|
|
System.out.printf(" FAIL %s%n", desc);
|
|
}
|
|
}
|
|
|
|
public static void main(String[] args) {
|
|
System.out.println("EdgesAddEdgeAlgorithm — CWE-407 unit test");
|
|
System.out.println("cargo-0002: Edges::add_edge Vec<Edge>.contains() → LinkedHashSet");
|
|
System.out.println();
|
|
|
|
// --- Correctness: no duplicate edges ---
|
|
{
|
|
SlowEdges slow = new SlowEdges();
|
|
FastEdges fast = new FastEdges();
|
|
|
|
Edge e1 = new Edge(EdgeKind.DEP, 1, true);
|
|
Edge e2 = new Edge(EdgeKind.DEP, 2, false);
|
|
Edge e3 = new Edge(EdgeKind.FEATURE, 3, true);
|
|
Edge e1dup = new Edge(EdgeKind.DEP, 1, true); // duplicate of e1
|
|
|
|
for (Edge e : List.of(e1, e2, e3, e1dup, e2, e3)) {
|
|
slow.addEdge(e);
|
|
fast.addEdge(e);
|
|
}
|
|
|
|
check("no-dup: slow drops duplicates", slow.edgeCount() == 3);
|
|
check("no-dup: fast drops duplicates", fast.edgeCount() == 3);
|
|
check("no-dup: same DEP edges",
|
|
slow.ofKind(EdgeKind.DEP).equals(fast.ofKind(EdgeKind.DEP)));
|
|
check("no-dup: same FEATURE edges",
|
|
slow.ofKind(EdgeKind.FEATURE).equals(fast.ofKind(EdgeKind.FEATURE)));
|
|
}
|
|
|
|
// --- Correctness: insertion order preserved ---
|
|
{
|
|
SlowEdges slow = new SlowEdges();
|
|
FastEdges fast = new FastEdges();
|
|
|
|
for (int i = 0; i < 10; i++) {
|
|
slow.addEdge(new Edge(EdgeKind.FEATURE, i, true));
|
|
fast.addEdge(new Edge(EdgeKind.FEATURE, i, true));
|
|
}
|
|
|
|
check("order: slow preserves insertion order",
|
|
slow.ofKind(EdgeKind.FEATURE).get(0).node == 0 &&
|
|
slow.ofKind(EdgeKind.FEATURE).get(9).node == 9);
|
|
check("order: fast preserves insertion order",
|
|
fast.ofKind(EdgeKind.FEATURE).get(0).node == 0 &&
|
|
fast.ofKind(EdgeKind.FEATURE).get(9).node == 9);
|
|
}
|
|
|
|
// --- Performance: O(E²) vs O(E) ---
|
|
{
|
|
// Simulate adding E unique FEATURE edges (no duplicates) — worst case for slow
|
|
// because each insertion must scan the entire existing bucket.
|
|
// Model: a package node with F feature edges, e.g. tokio with 50+ features.
|
|
int F = 200; // feature edge count per node (--graph-features mode, large workspace)
|
|
|
|
// Build edge list: F unique Feature edges
|
|
List<Edge> edges = new ArrayList<>();
|
|
for (int i = 0; i < F; i++) {
|
|
edges.add(new Edge(EdgeKind.FEATURE, i, true));
|
|
}
|
|
|
|
// Warm up JIT
|
|
for (int w = 0; w < 50; w++) {
|
|
SlowEdges s = new SlowEdges();
|
|
FastEdges f = new FastEdges();
|
|
for (Edge e : edges) { s.addEdge(e); f.addEdge(e); }
|
|
}
|
|
|
|
int RUNS = 2_000;
|
|
long slowContains = 0;
|
|
long fastInserts = 0;
|
|
|
|
long t0 = System.nanoTime();
|
|
for (int r = 0; r < RUNS; r++) {
|
|
SlowEdges s = new SlowEdges();
|
|
for (Edge e : edges) s.addEdge(e);
|
|
slowContains += s.containsChecks;
|
|
}
|
|
long slowNs = System.nanoTime() - t0;
|
|
|
|
long t1 = System.nanoTime();
|
|
for (int r = 0; r < RUNS; r++) {
|
|
FastEdges f = new FastEdges();
|
|
for (Edge e : edges) f.addEdge(e);
|
|
fastInserts += f.insertCalls;
|
|
}
|
|
long fastNs = System.nanoTime() - t1;
|
|
|
|
double ratio = (double) slowNs / fastNs;
|
|
long slowOpsPerRun = slowContains / RUNS;
|
|
long fastOpsPerRun = fastInserts / RUNS;
|
|
|
|
System.out.printf(" INFO F=%d slow_contains_checks=%d fast_inserts=%d ratio=%.1fx%n",
|
|
F, slowOpsPerRun, fastOpsPerRun, ratio);
|
|
|
|
// Slow: F unique edges → 0+1+2+...+(F-1) = F*(F-1)/2 contains checks total
|
|
long expectedSlowTotal = (long) F * (F - 1) / 2;
|
|
check("slow: contains checks = F*(F-1)/2 per run (triangular O(F²))",
|
|
slowOpsPerRun >= expectedSlowTotal);
|
|
// Fast: exactly F insert calls per run
|
|
check("fast: insert calls = F per run (O(F))",
|
|
fastOpsPerRun == F);
|
|
// Op ratio matches O(F²)/O(F) = O(F) = 200/2 = 100x difference in ops
|
|
check("slow op count >> fast op count (>= 50x)",
|
|
slowOpsPerRun >= fastOpsPerRun * 50);
|
|
}
|
|
|
|
System.out.println();
|
|
System.out.printf("%d/%d PASS%n", passed, total);
|
|
if (passed != total) System.exit(1);
|
|
}
|
|
}
|