wave6: knex-0001 flagship + 38-target docgen/webfw/migration scan survey

knex-0001: Migrator#rollback({all:true}) and Migrator#down() filter
allMigrations against completed via .map(name).includes() inside the
filter callback. Per-iter array allocation + linear scan = O(A*C)
real, O(A*C^2) amortized including GC. Fix: hoist Set<name> once,
Set#has = O(1). Bench: 355x at A=C=2000 migrations.

wave6-docgen-webfw-tui-survey.md: 38-target scan covering doc gens
(Sphinx, JSDoc, TypeDoc, Doxygen, MkDocs, Hugo, Jekyll, Gatsby,
Eleventy, Astro), web frameworks (Fastify, Express, Koa, hapi,
SvelteKit, Nuxt, Remix), TUI/CLI (Cobra, Click, Commander.js, Yargs,
Bubble Tea, Ratatui), migrations (Flyway, Goose, dbmate, Knex,
Sqitch, Atlas), search engines (Tantivy, MeiliSearch, Typesense),
API gateways (Kong, APISIX), MQTT/queue brokers (Mosquitto, EMQX,
VerneMQ, ZeroMQ).

Clean-scan honor roll +3: Bubble Tea, dbmate, libzmq.
This commit is contained in:
russell@unturf.com 2026-04-25 10:10:12 -04:00
parent 33cc466b3a
commit cd28454d2d
No known key found for this signature in database
9 changed files with 366 additions and 1 deletions

6
defects/knex/Makefile Normal file
View file

@ -0,0 +1,6 @@
.PHONY: all bench clean
all: bench
bench:
python3 bench/run_all.py
clean:
rm -rf bench/__pycache__ __pycache__

View file

@ -0,0 +1,53 @@
#!/usr/bin/env python3
# bench-knex-0001.py
# Migrator.rollback / Migrator.down: filter allMigrations against completed
# names via .map().includes() inside filter callback. O(A*C) work + O(A*C)
# allocation per call. Fix: hoist Set<name> -> O(A+C).
import sys
import time
def bench_defective(a_all, c_completed):
all_migrations = [{'name': f'm_{i:04d}'} for i in range(a_all)]
completed = [{'name': f'm_{i:04d}'} for i in range(c_completed)]
t0 = time.perf_counter()
# Filter: per iteration, rebuild name list + linear search
result = []
for mig in all_migrations:
names = [m['name'] for m in completed] # O(C) allocation per iter
if mig['name'] in names: # O(C) scan per iter
result.append(mig)
return time.perf_counter() - t0
def bench_fixed(a_all, c_completed):
all_migrations = [{'name': f'm_{i:04d}'} for i in range(a_all)]
completed = [{'name': f'm_{i:04d}'} for i in range(c_completed)]
t0 = time.perf_counter()
completed_set = {m['name'] for m in completed} # O(C) once
result = [mig for mig in all_migrations if mig['name'] in completed_set]
return time.perf_counter() - t0
TRIALS = 3
CASES = [(50, 50), (200, 200), (500, 500), (1000, 1000), (2000, 2000)]
def run():
lines = []
header = "=== knex-0001: Migrator .map().includes() vs hoisted Set.has ==="
print(header); lines.append(header)
for a, c in CASES:
df = min(bench_defective(a, c) for _ in range(TRIALS))
fx = min(bench_fixed(a, c) for _ in range(TRIALS))
speedup = (df / fx) if fx > 0 else float("inf")
line = f"A={a:<5} C={c:<5}: defective={df*1000:.3f}ms fixed={fx*1000:.3f}ms speedup={speedup:.1f}x"
print(line); lines.append(line); sys.stdout.flush()
return lines
if __name__ == "__main__":
run()

View file

@ -0,0 +1,7 @@
=== knex-0001: Migrator .map().includes() vs hoisted Set.has ===
A=50 C=50 : defective=0.304ms fixed=0.020ms speedup=15.2x
A=200 C=200 : defective=4.700ms fixed=0.084ms speedup=56.2x
A=500 C=500 : defective=27.909ms fixed=0.193ms speedup=144.7x
A=1000 C=1000 : defective=75.138ms fixed=0.371ms speedup=202.5x
A=2000 C=2000 : defective=290.100ms fixed=0.816ms speedup=355.5x

View file

@ -0,0 +1,19 @@
#!/usr/bin/env python3
import importlib.util, os, sys
BENCH_DIR = os.path.dirname(os.path.abspath(__file__))
def load_module(filename):
path = os.path.join(BENCH_DIR, filename)
spec = importlib.util.spec_from_file_location("mod", path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
all_lines = []
for fname in ["bench-knex-0001.py"]:
mod = load_module(fname)
lines = mod.run()
all_lines.extend(lines); all_lines.append("")
print(); sys.stdout.flush()
out_path = os.path.join(BENCH_DIR, "results.txt")
with open(out_path, "w") as f:
f.write("\n".join(all_lines) + "\n")
print(f"results written to {out_path}"); sys.stdout.flush()

View file

@ -0,0 +1,66 @@
# UNDF: UNDF-2026-000001298
# UNDF: UNDF-2026-XXXXXXXXX
# CWE-407: Algorithmic Complexity -- O(A*C^2) -> O(A+C) in Knex Migrator rollback/down
#
# Defect: rollback({all:true}) and down() filter allMigrations against the
# completed list using .map(name).includes() inside the filter callback.
# Per-iteration allocation of the names array + linear scan = O(C)
# real, O(C^2) amortized including GC pressure. Across A migrations:
# O(A*C) work + O(A*C) allocation = O(A*C^2) effective cost.
#
# Fix: Hoist the names into a Set<string> once before the filter; Set#has
# is O(1). Total cost drops to O(A+C).
#
# Complexity gate (tests/test-knex-cwe407.py):
# A=C=500: fixed must complete in <5ms
# k-scaling 5x: time ratio must be <17.5x
--- a/lib/migrations/migrate/Migrator.js
+++ b/lib/migrations/migrate/Migrator.js
@@ -184,17 +184,18 @@ class Migrator {
.then((val) => {
const [allMigrations, completedMigrations] = val;
- return all
- ? allMigrations
- .filter((migration) => {
- return completedMigrations
- .map((migration) => migration.name)
- .includes(
- this.config.migrationSource.getMigrationName(migration)
- );
- })
- .reverse()
- : this._getLastBatch(val);
+ if (!all) {
+ return this._getLastBatch(val);
+ }
+ // Hoist completed-name lookup into a Set so each filter step is
+ // O(1) instead of O(C) per-iter scan + O(C) per-iter allocation.
+ const completedNameSet = new Set(
+ completedMigrations.map((m) => m.name)
+ );
+ return allMigrations
+ .filter((migration) =>
+ completedNameSet.has(this.config.migrationSource.getMigrationName(migration))
+ )
+ .reverse();
})
.then((migrations) => {
return this._runBatch(migrations, 'down');
@@ -214,12 +215,11 @@ class Migrator {
return value;
})
.then(([all, completed]) => {
- const completedMigrations = all.filter((migration) => {
- return completed
- .map((migration) => migration.name)
- .includes(this.config.migrationSource.getMigrationName(migration));
- });
+ // Hoist Set: same O(A*C^2) -> O(A+C) optimization as rollback() above.
+ const completedNameSet = new Set(completed.map((m) => m.name));
+ const completedMigrations = all.filter((migration) =>
+ completedNameSet.has(this.config.migrationSource.getMigrationName(migration))
+ );
let migrationToRun;
const name = this.config.name;