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.
64 lines
3.1 KiB
Diff
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)
|
|
}
|