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:
parent
33cc466b3a
commit
cd28454d2d
9 changed files with 366 additions and 1 deletions
6
defects/knex/Makefile
Normal file
6
defects/knex/Makefile
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
.PHONY: all bench clean
|
||||
all: bench
|
||||
bench:
|
||||
python3 bench/run_all.py
|
||||
clean:
|
||||
rm -rf bench/__pycache__ __pycache__
|
||||
53
defects/knex/bench/bench-knex-0001.py
Normal file
53
defects/knex/bench/bench-knex-0001.py
Normal 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()
|
||||
7
defects/knex/bench/results.txt
Normal file
7
defects/knex/bench/results.txt
Normal 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
|
||||
|
||||
19
defects/knex/bench/run_all.py
Normal file
19
defects/knex/bench/run_all.py
Normal 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()
|
||||
|
|
@ -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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue