java-topology/defects/testcafe/bench/bench-testcafe-0001.py
russell@unturf.com b79fddfb51 browser-automation wave 2: testcafe-0001 + webdriverio-0002
testcafe-0001: Selector filterNodes (string-filter branch) and
  expandSelectorResults both dedup via Array.indexOf on growing result
  arrays. filterNodes: O(N*M) per selector filter. expandSelectorResults:
  O(N^2 * K^2) worst case when derivatives unique. Fix: Set<Node> keyed
  by object identity. Bench: 398x at N=2000 filter, 1966x at N=K=150
  expand.

webdriverio-0002: MSPO aggregator dedups per-test entries via Array.find
  on growing bucket array. O(N^2) per test bucket, same pattern repeats
  in unknown-suite merger. Fix: companion Map<bucketKey, Set<selector>>
  for O(1) dedup. Bench: 493x at N=2000.

UNDF IDs: 1290 (testcafe), 1291 (webdriverio-0002). All 17 tests pass.
2026-04-22 18:31:53 -04:00

110 lines
3.5 KiB
Python

#!/usr/bin/env python3
# bench-testcafe-0001.py
# Selector.filterNodes: matchingArr.indexOf(node) > -1 per node (O(N*M))
# vs matchingSet.has(node) (O(N+M)).
# Selector.expandSelectorResults: result.indexOf(deriv) < 0 on growing result
# (worst O(N*K*(N*K))) vs parallel seen Set (O(N*K)).
import sys
import time
class Node:
"""Unique object acting as DOM Node for identity-based dedup."""
__slots__ = ("id",)
def __init__(self, i):
self.id = i
def bench_filter_defective(n, m):
"""matchingArr.indexOf(node) > -1 per node."""
matching = [Node(i) for i in range(m)]
nodes = matching + [Node(1000 + i) for i in range(n - m)] # N total, M match
t0 = time.perf_counter()
matchingArr = [x for x in matching]
# lambda filter captures matchingArr
filter_fn = lambda node: matchingArr.index(node) > -1 if node in matchingArr else False
# simpler: node in matchingArr (list) = O(M) per check
result = []
for node in nodes:
if node in matchingArr:
result.append(node)
return time.perf_counter() - t0
def bench_filter_fixed(n, m):
"""matchingSet.has(node)."""
matching = [Node(i) for i in range(m)]
nodes = matching + [Node(1000 + i) for i in range(n - m)]
t0 = time.perf_counter()
matchingSet = set(matching)
result = []
for node in nodes:
if node in matchingSet:
result.append(node)
return time.perf_counter() - t0
def bench_expand_defective(n, k):
"""result.indexOf(deriv) < 0 against growing result."""
# All derivatives unique so result grows to N*K
nodes = [Node(i) for i in range(n)]
derivatives_per_node = [[Node(i * 10000 + j) for j in range(k)] for i in range(n)]
t0 = time.perf_counter()
result = []
for i in range(n):
for deriv in derivatives_per_node[i]:
if deriv not in result: # O(|result|)
result.append(deriv)
return time.perf_counter() - t0
def bench_expand_fixed(n, k):
"""Parallel seen Set."""
nodes = [Node(i) for i in range(n)]
derivatives_per_node = [[Node(i * 10000 + j) for j in range(k)] for i in range(n)]
t0 = time.perf_counter()
seen = set()
result = []
for i in range(n):
for deriv in derivatives_per_node[i]:
if deriv not in seen:
seen.add(deriv)
result.append(deriv)
return time.perf_counter() - t0
TRIALS = 3
FILTER_CASES = [(100, 50), (500, 250), (1000, 500), (2000, 1000)]
EXPAND_CASES = [(20, 20), (50, 50), (100, 100), (150, 150)]
def run():
lines = []
h = "=== testcafe-0001 filterNodes: indexOf vs Set.has ==="
print(h); lines.append(h)
for n, m in FILTER_CASES:
d = min(bench_filter_defective(n, m) for _ in range(TRIALS))
f = min(bench_filter_fixed(n, m) for _ in range(TRIALS))
speedup = (d / f) if f > 0 else float("inf")
l = f"N={n:<5} M={m:<5}: defective={d*1000:.3f}ms fixed={f*1000:.3f}ms speedup={speedup:.1f}x"
print(l); lines.append(l); sys.stdout.flush()
h = "=== testcafe-0001 expandSelectorResults: indexOf vs Set.has ==="
print(h); lines.append(h)
for n, k in EXPAND_CASES:
d = min(bench_expand_defective(n, k) for _ in range(TRIALS))
f = min(bench_expand_fixed(n, k) for _ in range(TRIALS))
speedup = (d / f) if f > 0 else float("inf")
l = f"N={n:<4} K={k:<4} (total {n*k}): defective={d*1000:.3f}ms fixed={f*1000:.3f}ms speedup={speedup:.1f}x"
print(l); lines.append(l); sys.stdout.flush()
return lines
if __name__ == "__main__":
run()