java-topology/defects/webpack/unit/WebpackHmrTest.java
russell@unturf.com 0a580b313d undefect. CWE-407 — 63 sites patched across 27 ecosystems
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.
2026-03-26 17:11:57 -04:00

374 lines
14 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package unit;
import java.util.*;
/**
* Unit test for webpack-0001/0002/0003: CWE-407 in webpack HMR runtime.
*
* webpack-0001 (MEDIUM-HIGH):
* File: lib/hmr/JavascriptHotModuleReplacement.runtime.js:74
* Symbol: getAffectedModuleEffects — `outdatedModules.indexOf(parentId)`
* Defect: BFS traversal over a module dependency graph uses an Array for the
* outdatedModules visited set. For each queued module, for each parent,
* indexOf scans the entire array: O(M) per check, O(M²) total.
* Fix: Shadow with a Set; check becomes Set.has(), O(1).
*
* webpack-0002 (MEDIUM):
* File: lib/hmr/JavascriptHotModuleReplacement.runtime.js:101
* Symbol: addAllToSet — `a.indexOf(item)`
* Defect: Result-merging helper deduplicates by scanning the accumulator
* array on every insertion: O(N) per item, O(N²) total for N items.
* Fix: Maintain a companion Set on the accumulator array; check is O(1).
*
* webpack-0003 (MEDIUM):
* File: lib/hmr/HotModuleReplacement.runtime.js:60,67
* Symbol: createRequire — `parents.indexOf(moduleId)`, `me.children.indexOf(request)`
* Defect: Every require() call in the HMR hot path deduplicates parents and
* children using indexOf on plain arrays: O(P) and O(C) per call.
* Fix: Attach Set companions (_parentsSet, _childrenSet); O(1) per check.
*
* Modeled here in Java:
* JS Array + indexOf ≡ List<Integer> + contains/indexOf (defective)
* JS Set + has ≡ java.util.Set + contains (fixed)
* Comparison counts tracked at the membership-test site.
*
* Expected at M=200:
* defective BFS comparisons ≈ M² = 40 000
* fixed BFS comparisons ≈ M = 200
* ratio > 10×
*/
public class WebpackHmrTest {
// =========================================================================
// webpack-0001 model: BFS with outdatedModules as Array vs Set
// =========================================================================
/**
* Defective BFS: outdatedModules is a plain List.
* For each (module, parent) pair we call list.contains(), which is O(size).
* comparisons counts every element examined during indexOf scans.
*/
static class DefectiveBfs {
long comparisons = 0;
/**
* Simulate getAffectedModuleEffects: process a chain of M modules where
* each module has one parent, and the parent is always "not yet in
* outdatedModules" (worst-case: no early-exit, full scan every time).
*
* Graph: 0 → 1 → 2 → ... → M-1 (child → parent direction).
* BFS starts from module 0, propagates up.
*/
List<Integer> run(int M) {
List<Integer> outdatedModules = new ArrayList<>();
outdatedModules.add(0);
// Queue: (moduleId). We process each and check its single parent.
Queue<Integer> queue = new ArrayDeque<>();
queue.add(0);
while (!queue.isEmpty()) {
int moduleId = queue.poll();
int parentId = moduleId + 1; // parent in linear chain
if (parentId >= M) continue; // no parent beyond chain end
// outdatedModules.indexOf(parentId) — O(current size) scan
boolean found = false;
for (int x : outdatedModules) {
comparisons++;
if (x == parentId) { found = true; break; }
}
if (found) continue;
outdatedModules.add(parentId);
queue.add(parentId);
}
return outdatedModules;
}
}
/**
* Fixed BFS: shadow outdatedModules with a Set for O(1) membership.
* comparisons counts one hash-probe per Set.contains() call.
*/
static class FixedBfs {
long comparisons = 0;
List<Integer> run(int M) {
List<Integer> outdatedModules = new ArrayList<>();
Set<Integer> outdatedSet = new HashSet<>();
outdatedModules.add(0);
outdatedSet.add(0);
Queue<Integer> queue = new ArrayDeque<>();
queue.add(0);
while (!queue.isEmpty()) {
int moduleId = queue.poll();
int parentId = moduleId + 1;
if (parentId >= M) continue;
// Set.contains(parentId) — O(1)
comparisons++;
if (outdatedSet.contains(parentId)) continue;
outdatedModules.add(parentId);
outdatedSet.add(parentId);
queue.add(parentId);
}
return outdatedModules;
}
}
// =========================================================================
// webpack-0002 model: addAllToSet with Array indexOf vs Set companion
// =========================================================================
/**
* Defective addAllToSet: accumulator is a plain List; every insertion
* scans the list to deduplicate.
* comparisons counts every element examined during indexOf scans.
*/
static long addAllToSetDefective(List<Integer> a, List<Integer> b,
long[] comparisons) {
for (int item : b) {
// a.indexOf(item) — O(a.size()) scan
boolean found = false;
for (int x : a) {
comparisons[0]++;
if (x == item) { found = true; break; }
}
if (!found) a.add(item);
}
return comparisons[0];
}
/**
* Fixed addAllToSet: companion Set maintained alongside the List.
* comparisons counts one hash-probe per Set.contains() call.
*/
static long addAllToSetFixed(List<Integer> a, Set<Integer> aSet,
List<Integer> b, long[] comparisons) {
for (int item : b) {
comparisons[0]++; // O(1) Set.contains
if (!aSet.contains(item)) {
a.add(item);
aSet.add(item);
}
}
return comparisons[0];
}
// =========================================================================
// webpack-0003 model: parents/children dedup in require() hot path
// =========================================================================
/**
* Defective require(): parents and children deduped via indexOf on plain arrays.
* Simulates N require() calls where each call checks both parents and children.
* comparisons counts every element examined during indexOf scans.
*/
static long simulateRequireDefective(int N) {
long comparisons = 0;
List<Integer> parents = new ArrayList<>();
List<Integer> children = new ArrayList<>();
for (int moduleId = 0; moduleId < N; moduleId++) {
int request = moduleId + 1000; // distinct child module IDs
// parents.indexOf(moduleId) — O(parents.size())
boolean foundParent = false;
for (int x : parents) {
comparisons++;
if (x == moduleId) { foundParent = true; break; }
}
if (!foundParent) parents.add(moduleId);
// me.children.indexOf(request) — O(children.size())
boolean foundChild = false;
for (int x : children) {
comparisons++;
if (x == request) { foundChild = true; break; }
}
if (!foundChild) children.add(request);
}
return comparisons;
}
/**
* Fixed require(): Set companions for O(1) dedup.
* comparisons counts one hash-probe per check.
*/
static long simulateRequireFixed(int N) {
long comparisons = 0;
List<Integer> parents = new ArrayList<>();
Set<Integer> parentsSet = new HashSet<>();
List<Integer> children = new ArrayList<>();
Set<Integer> childrenSet = new HashSet<>();
for (int moduleId = 0; moduleId < N; moduleId++) {
int request = moduleId + 1000;
// parentsSet.contains(moduleId) — O(1)
comparisons++;
if (!parentsSet.contains(moduleId)) {
parents.add(moduleId);
parentsSet.add(moduleId);
}
// childrenSet.contains(request) — O(1)
comparisons++;
if (!childrenSet.contains(request)) {
children.add(request);
childrenSet.add(request);
}
}
return comparisons;
}
// =========================================================================
// Tests
// =========================================================================
/**
* Test 1 — Correctness: defective and fixed BFS produce identical module sets.
*/
static void testBfsCorrectnessMatch() {
int M = 50;
DefectiveBfs def = new DefectiveBfs();
FixedBfs fix = new FixedBfs();
List<Integer> defResult = def.run(M);
List<Integer> fixResult = fix.run(M);
Set<Integer> defSet = new HashSet<>(defResult);
Set<Integer> fixSet = new HashSet<>(fixResult);
assert defSet.equals(fixSet)
: "BFS output mismatch: defective=" + defSet + " fixed=" + fixSet;
assert defResult.size() == M
: "expected " + M + " outdated modules; got " + defResult.size();
System.out.println("PASS testBfsCorrectnessMatch");
}
/**
* Test 2 — webpack-0001: ratio of defective vs fixed BFS comparisons > 10x at M=200.
*
* Defective: each of M modules triggers a scan of the growing outdatedModules
* list (size 0..M-1), summing to M*(M-1)/2 comparisons ~ O(M²).
* Fixed: each module triggers exactly 1 Set probe, summing to M ~ O(M).
*/
static void testBfsRatioAtScale() {
int M = 200;
DefectiveBfs def = new DefectiveBfs();
FixedBfs fix = new FixedBfs();
def.run(M);
fix.run(M);
long defComp = def.comparisons;
long fixComp = fix.comparisons;
double ratio = (double) defComp / fixComp;
// Defective: sum 0+1+...+(M-1) = M*(M-1)/2
long expectedDef = (long) M * (M - 1) / 2;
assert defComp == expectedDef
: "defective BFS comparisons should be M*(M-1)/2=" + expectedDef
+ "; got " + defComp;
// Fixed: M-1 probes (one per module that has a parent to check;
// the last module in the chain has no parent, so no probe is issued)
assert fixComp == M - 1
: "fixed BFS comparisons should be M-1=" + (M - 1) + "; got " + fixComp;
assert ratio > 10.0
: "ratio should be >10x at M=200; got " + ratio;
System.out.printf(
"PASS testBfsRatioAtScale (defective=%d, fixed=%d, ratio=%.1fx)%n",
defComp, fixComp, ratio);
}
/**
* Test 3 — webpack-0002: addAllToSet defective is O(N²), fixed is O(N).
*
* Merge N unique items into an accumulator. Defective scans the accumulator
* before each insert (size 0..N-1), costing N*(N-1)/2. Fixed costs N probes.
*/
static void testAddAllToSetRatio() {
int N = 200;
// Build source list of N unique items
List<Integer> source = new ArrayList<>();
for (int i = 0; i < N; i++) source.add(i);
// Defective
long[] defComp = {0};
List<Integer> defAcc = new ArrayList<>();
addAllToSetDefective(defAcc, source, defComp);
// Fixed
long[] fixComp = {0};
List<Integer> fixAcc = new ArrayList<>();
Set<Integer> fixSet = new HashSet<>();
addAllToSetFixed(fixAcc, fixSet, source, fixComp);
assert defAcc.size() == N && fixAcc.size() == N
: "both accumulators should have " + N + " items";
assert new HashSet<>(defAcc).equals(new HashSet<>(fixAcc))
: "accumulator contents differ";
double ratio = (double) defComp[0] / fixComp[0];
assert ratio > 10.0
: "addAllToSet ratio should be >10x at N=200; got " + ratio;
System.out.printf(
"PASS testAddAllToSetRatio (defective=%d, fixed=%d, ratio=%.1fx)%n",
defComp[0], fixComp[0], ratio);
}
/**
* Test 4 — webpack-0003: require() hot path defective is O(N²), fixed is O(N).
*
* N require() calls, each adding a unique moduleId to parents and a unique
* request to children. Defective: scans grow 0..N-1 for each; fixed: O(1) probes.
*/
static void testRequireHotPathRatio() {
int N = 200;
long defComp = simulateRequireDefective(N);
long fixComp = simulateRequireFixed(N);
double ratio = (double) defComp / fixComp;
// Defective: two indexOf scans per call, sizes grow 0..N-1
// Sum for parents: 0+1+...+(N-1) = N*(N-1)/2; same for children.
long expectedDef = (long) N * (N - 1); // two arrays: 2 × N*(N-1)/2
assert defComp == expectedDef
: "defective require comparisons should be N*(N-1)=" + expectedDef
+ "; got " + defComp;
// Fixed: exactly 2 probes per call (one per Set)
long expectedFix = 2L * N;
assert fixComp == expectedFix
: "fixed require comparisons should be 2*N=" + expectedFix
+ "; got " + fixComp;
assert ratio > 10.0
: "require() ratio should be >10x at N=200; got " + ratio;
System.out.printf(
"PASS testRequireHotPathRatio (defective=%d, fixed=%d, ratio=%.1fx)%n",
defComp, fixComp, ratio);
}
// =========================================================================
public static void main(String[] args) {
testBfsCorrectnessMatch();
testBfsRatioAtScale();
testAddAllToSetRatio();
testRequireHotPathRatio();
System.out.println("All webpack HMR tests passed.");
}
}