java-topology/defects/webdriverio/patch/webdriverio-0002-mspo-aggregator-selector-dedup-find.patch
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

64 lines
3.1 KiB
Diff

# UNDF: UNDF-2026-000001291
# UNDF: UNDF-2026-XXXXXXXXX
# CWE-407: Algorithmic Complexity -- O(N^2) -> O(N) in MSPO aggregator dedup
#
# Defect: aggregator dedup runs .find(d => d.selector === data.selector) on
# grouped[specFile][suiteName][testName] for every collected entry. N
# entries per test bucket gives O(N^2). Same pattern repeats at line 369
# in the unknown-suite merger.
#
# Fix: Carry a Set<string> of observed selectors per test bucket; the existing
# array remains for output order and downstream consumers. Per-entry cost
# drops from O(N) to O(1). For the unknown-suite merger, pre-build the Set
# from the destination bucket once per merger pass.
#
# Complexity gate (tests/test-webdriverio-cwe407.py):
# k-scaling 5x: time ratio must be <17.5x (O(k) ~=5x, not O(k^2) ~=25x)
# N=1000 per test bucket: must complete in <5ms
--- a/packages/wdio-appium-service/src/mobileSelectorPerformanceOptimizer/aggregator.ts
+++ b/packages/wdio-appium-service/src/mobileSelectorPerformanceOptimizer/aggregator.ts
@@ -330,6 +330,11 @@ export function aggregateSelectorData(
const grouped: GroupedData = {}
+ // Companion Map<specFile::suiteName::testName, Set<selector>> gives O(1)
+ // dedup per entry; prior impl scanned the data array via .find per entry,
+ // giving O(N^2) per test bucket.
+ const seenByBucket = new Map<string, Set<string>>()
+
for (const data of collectedData) {
const { specFile, suiteName, testName } = data
@@ -340,10 +345,13 @@ export function aggregateSelectorData(
grouped[specFile][suiteName][testName] = []
}
- const existing = grouped[specFile][suiteName][testName].find(d => d.selector === data.selector)
-
- if (!existing) {
+ const bucketKey = `${specFile}::${suiteName}::${testName}`
+ let seen = seenByBucket.get(bucketKey)
+ if (!seen) {
+ seen = new Set<string>()
+ seenByBucket.set(bucketKey, seen)
+ }
+ if (!seen.has(data.selector)) {
+ seen.add(data.selector)
grouped[specFile][suiteName][testName].push(data)
}
}
@@ -365,9 +373,12 @@ export function aggregateSelectorData(
suites[knownSuiteName][testName] = []
}
+ // Pre-build a Set from the destination bucket once per
+ // merger pass so the inner loop does O(1) dedup instead
+ // of .find across the growing destination array.
+ const destSeen = new Set(suites[knownSuiteName][testName].map(d => d.selector))
for (const data of unknownSuite[testName]) {
- const existing = suites[knownSuiteName][testName].find(d => d.selector === data.selector)
- if (!existing) {
+ if (!destSeen.has(data.selector)) {
+ destSeen.add(data.selector)
data.suiteName = knownSuiteName
suites[knownSuiteName][testName].push(data)
}