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.
This commit is contained in:
parent
9a0253e724
commit
b79fddfb51
16 changed files with 827 additions and 17 deletions
|
|
@ -0,0 +1,64 @@
|
|||
# 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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue