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.
3 KiB
webdriverio-0002: MSPO aggregator — O(N²) selector dedup via Array.find
Target: webdriverio/webdriverio
Severity: MEDIUM
CWE: CWE-407 (Inefficient Algorithmic Complexity)
MOAD: MOAD-0001 (A Sedimentary Defect)
File: packages/wdio-appium-service/src/mobileSelectorPerformanceOptimizer/aggregator.ts:343, 369
Language: TypeScript
Status: open
Description
The Mobile Selector Performance Optimizer (MSPO) aggregates selector
performance data across tests. Its aggregation pipeline dedups per-test
selector entries via Array.find(d => d.selector === data.selector) inside
the per-entry accumulation loop. For N collected entries per test, dedup is
O(N²). Large mobile test suites that exercise many unique selectors per test
hit this scaling.
A second instance of the same pattern lives in the unknown-suite merger loop at line 369, running during post-test attribution when MSPO stitches orphan test entries back into their parent suites.
Root Cause
// aggregator.ts:341-347 per-entry accumulation
for (const data of collectedData) {
if (!grouped[specFile][suiteName][testName]) {
grouped[specFile][suiteName][testName] = [];
}
const existing = grouped[specFile][suiteName][testName]
.find(d => d.selector === data.selector); // O(N) scan per entry
if (!existing) {
grouped[specFile][suiteName][testName].push(data);
}
}
// Total: O(N^2) per test
// aggregator.ts:367-374 unknown-suite merger
for (const data of unknownSuite[testName]) {
const existing = suites[knownSuiteName][testName]
.find(d => d.selector === data.selector); // O(N) scan per entry
if (!existing) {
data.suiteName = knownSuiteName;
suites[knownSuiteName][testName].push(data);
}
}
Fix
Maintain a parallel Map<string, SelectorPerformanceData> keyed by selector
alongside the array. Lookup and insert drop to amortized O(1). Array is kept
for output order and downstream consumers that iterate.
// Replace the bare array with a { data: [], bySelector: Map<string, Data> } tuple
// or carry a companion Map<testName, Set<selector>> at the aggregator level.
const seenSelectors = new Set<string>(); // per test bucket
for (const data of collectedData) {
if (!seenSelectors.has(data.selector)) {
seenSelectors.add(data.selector);
grouped[specFile][suiteName][testName].push(data);
}
}
For the unknown-suite merger, pre-build a Set<string> of existing selector
keys in the destination bucket, then iterate source entries with O(1) lookup.
Severity Note
MSPO is opt-in tooling activated via the mobileSelectorPerformanceOptimizer
service. Impact scales with test count × unique selectors per test. A 1000-
test suite with 50 unique selectors per test aggregates 50,000 entries;
current path is O(N²) = 2.5 billion comparisons. The fix restores linear
behavior.
Complexity Gate
- N=1000 entries per test bucket: fixed must complete in <5ms
- k-scaling 5×: time ratio must be <17.5× (O(k) ≈5×, not O(k²) ≈25×)