Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com Patches, unit tests, benchmarks, whitepaper, and outreach briefs. Public domain — no copyright claimed. Use freely.
151 lines
6.9 KiB
Java
151 lines
6.9 KiB
Java
package bench;
|
|
|
|
import com.google.common.collect.HashMultimap;
|
|
import com.google.common.collect.Multimap;
|
|
import java.util.*;
|
|
import java.util.function.Consumer;
|
|
|
|
/**
|
|
* Benchmark for minecraft-0001: DependencySorter.isCyclic exponential defect.
|
|
*
|
|
* BEFORE: no visited set — O(E^D) on diamond dependency graphs
|
|
* AFTER: visited set — O(E) per isCyclic call
|
|
*
|
|
* Graph topology: diamond chain of depth D
|
|
* root → branch_left_d, branch_right_d
|
|
* branch_left_d → branch_left_(d-1) → ... → leaf
|
|
* branch_right_d → branch_right_(d-1) → ... → leaf
|
|
* (shared leaf causes exponential revisiting without visited set)
|
|
*
|
|
* In Minecraft: this is the tag dependency structure of modpack tag inheritance.
|
|
* Tags like #c:ingots depended upon by both #create:ingots and #ae2:ingots
|
|
* form exactly this diamond pattern, with depth proportional to mod count.
|
|
*/
|
|
public class DependencySorterBenchmark {
|
|
|
|
static final int WARMUP = 500;
|
|
static final int TRIALS = 2000;
|
|
|
|
// ── Simple tag entry for benchmarking ─────────────────────────────────────
|
|
record TagEntry(List<String> required) implements SorterEntry {
|
|
public void visitRequiredDependencies(Consumer<String> c) { required.forEach(c); }
|
|
public void visitOptionalDependencies(Consumer<String> c) {}
|
|
}
|
|
|
|
interface SorterEntry {
|
|
void visitRequiredDependencies(Consumer<String> c);
|
|
void visitOptionalDependencies(Consumer<String> c);
|
|
}
|
|
|
|
// ── Build diamond dependency graph of depth D ─────────────────────────────
|
|
// Structure: root → L1, R1; L1 → L2, R2; ... but all converge on shared leaf
|
|
// Creates 2^D paths from root to the shared base tag
|
|
static Map<String, List<String>> buildDiamondTags(int depth) {
|
|
Map<String, List<String>> deps = new LinkedHashMap<>();
|
|
// shared base at bottom
|
|
deps.put("base", List.of());
|
|
// each level has left and right that both depend on the level below
|
|
String prevLeft = "base", prevRight = "base";
|
|
for (int d = 1; d <= depth; d++) {
|
|
String left = "left_" + d;
|
|
String right = "right_" + d;
|
|
deps.put(left, List.of(prevLeft, prevRight));
|
|
deps.put(right, List.of(prevLeft, prevRight));
|
|
prevLeft = left;
|
|
prevRight = right;
|
|
}
|
|
deps.put("root", List.of(prevLeft, prevRight));
|
|
return deps;
|
|
}
|
|
|
|
// ── BEFORE: isCyclic with no visited set ──────────────────────────────────
|
|
static boolean isCyclicBefore(Multimap<String, String> deps, String from, String to) {
|
|
Collection<String> d = deps.get(to);
|
|
if (d.contains(from)) return true;
|
|
return d.stream().anyMatch(dep -> isCyclicBefore(deps, from, dep));
|
|
}
|
|
|
|
static void addDepBefore(Multimap<String, String> deps, String from, String to) {
|
|
if (!isCyclicBefore(deps, from, to)) deps.put(from, to);
|
|
}
|
|
|
|
static long runBefore(Map<String, List<String>> tagDeps) {
|
|
HashMultimap<String, String> deps = HashMultimap.create();
|
|
for (Map.Entry<String, List<String>> e : tagDeps.entrySet())
|
|
for (String dep : e.getValue())
|
|
addDepBefore(deps, e.getKey(), dep);
|
|
return deps.size();
|
|
}
|
|
|
|
// ── AFTER: isCyclic with visited set ──────────────────────────────────────
|
|
static boolean isCyclicAfter(Multimap<String, String> deps, String from, String to, Set<String> visited) {
|
|
if (!visited.add(to)) return false;
|
|
Collection<String> d = deps.get(to);
|
|
if (d.contains(from)) return true;
|
|
return d.stream().anyMatch(dep -> isCyclicAfter(deps, from, dep, visited));
|
|
}
|
|
|
|
static void addDepAfter(Multimap<String, String> deps, String from, String to) {
|
|
if (!isCyclicAfter(deps, from, to, new HashSet<>())) deps.put(from, to);
|
|
}
|
|
|
|
static long runAfter(Map<String, List<String>> tagDeps) {
|
|
HashMultimap<String, String> deps = HashMultimap.create();
|
|
for (Map.Entry<String, List<String>> e : tagDeps.entrySet())
|
|
for (String dep : e.getValue())
|
|
addDepAfter(deps, e.getKey(), dep);
|
|
return deps.size();
|
|
}
|
|
|
|
// ── Main ──────────────────────────────────────────────────────────────────
|
|
public static void main(String[] args) {
|
|
System.out.println("=== minecraft-0001: DependencySorter.isCyclic Benchmark ===");
|
|
System.out.println("Graph: diamond dependency chain (depth D)");
|
|
System.out.println("Each level doubles the paths to the shared base tag.");
|
|
System.out.println("Models modpack tag inheritance (e.g. #c:ingots diamond patterns).");
|
|
System.out.println();
|
|
System.out.printf("%-6s %-8s %-14s %-14s %-10s%n",
|
|
"Depth", "Tags", "BEFORE ns", "AFTER ns", "Speedup");
|
|
System.out.println("-".repeat(60));
|
|
|
|
int[] depths = {2, 4, 6, 8, 10, 12, 14, 16};
|
|
for (int d : depths) {
|
|
Map<String, List<String>> tags = buildDiamondTags(d);
|
|
int tagCount = tags.size();
|
|
|
|
// warmup — skip deep depths for before (stack overflow risk)
|
|
if (d <= 14) {
|
|
for (int i = 0; i < WARMUP; i++) runBefore(tags);
|
|
}
|
|
for (int i = 0; i < WARMUP; i++) runAfter(tags);
|
|
|
|
// time BEFORE (skip if too deep — stack overflow from exponential recursion)
|
|
long beforeNs = -1;
|
|
if (d <= 14) {
|
|
long t0 = System.nanoTime();
|
|
for (int i = 0; i < TRIALS; i++) runBefore(tags);
|
|
beforeNs = (System.nanoTime() - t0) / TRIALS;
|
|
}
|
|
|
|
// time AFTER
|
|
long t0 = System.nanoTime();
|
|
for (int i = 0; i < TRIALS; i++) runAfter(tags);
|
|
long afterNs = (System.nanoTime() - t0) / TRIALS;
|
|
|
|
if (beforeNs >= 0) {
|
|
double speedup = (double) beforeNs / afterNs;
|
|
System.out.printf("%-6d %-8d %-14d %-14d %.1fx%n",
|
|
d, tagCount, beforeNs, afterNs, speedup);
|
|
} else {
|
|
System.out.printf("%-6d %-8d %-14s %-14d (overflow)%n",
|
|
d, tagCount, "OVERFLOW", afterNs);
|
|
}
|
|
}
|
|
|
|
System.out.println();
|
|
System.out.println("Note: depth 16+ causes stack overflow BEFORE the fix — the");
|
|
System.out.println("recursion tree is 2^16 = 65,536 nodes deep with no visited set.");
|
|
System.out.println("A typical large modpack (Create + AE2 + Mekanism + Thermal) has");
|
|
System.out.println("diamond chains of depth 8-12 across thousands of cross-mod tags.");
|
|
}
|
|
}
|