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
93
docs/tickets/testcafe-0001-selector-filter-expand-indexof.md
Normal file
93
docs/tickets/testcafe-0001-selector-filter-expand-indexof.md
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
# testcafe-0001: selector filterNodes + expandSelectorResults — O(N×M) indexOf per node
|
||||
|
||||
**Target:** DevExpress/testcafe
|
||||
**Severity:** MEDIUM-HIGH
|
||||
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
|
||||
**MOAD:** MOAD-0001 (A Sedimentary Defect)
|
||||
**File:** `src/client-functions/selectors/add-api.js:30-48, 51-72`
|
||||
**Language:** JavaScript
|
||||
**Status:** open
|
||||
|
||||
## Description
|
||||
|
||||
TestCafe's `Selector` filter and derivative expansion run in the browser as
|
||||
client functions. `filterNodes` dedups query results against a CSS match list
|
||||
via `matchingArr.indexOf(node) > -1` inside a per-node loop, giving O(N×M)
|
||||
where N is input nodes and M is CSS matches. `expandSelectorResults` walks
|
||||
N origin nodes, expands each into K derivatives, and dedups each derivative
|
||||
against the growing `result` array via `result.indexOf(...) < 0` — O(N×K×R)
|
||||
where R grows up to N×K.
|
||||
|
||||
Every `Selector('.card').filter(x)` in every test hits this path. Pages with
|
||||
large DOM trees and chained selectors amplify the cost.
|
||||
|
||||
## Root Cause
|
||||
|
||||
```javascript
|
||||
// add-api.js:18-49 filterNodes, string-filter branch
|
||||
const matching = querySelectorRoot.querySelectorAll(filter);
|
||||
const matchingArr = [];
|
||||
for (let i = 0; i < matching.length; i++)
|
||||
matchingArr.push(matching[i]);
|
||||
filter = node => matchingArr.indexOf(node) > -1; // O(M) per filter call
|
||||
|
||||
for (let j = 0; j < nodes.length; j++) {
|
||||
if (filter(nodes[j], j, originNode, ...filterArgs)) // inner O(M)
|
||||
result.push(nodes[j]);
|
||||
}
|
||||
// Total: O(N * M)
|
||||
|
||||
// add-api.js:51-72 expandSelectorResults
|
||||
for (let i = 0; i < nodes.length; i++) { // O(N)
|
||||
const derivativeNodes = populateDerivativeNodes(nodes[i]);
|
||||
if (derivativeNodes) {
|
||||
for (let j = 0; j < derivativeNodes.length; j++) { // O(K)
|
||||
if (result.indexOf(derivativeNodes[j]) < 0) // O(|result|)
|
||||
result.push(derivativeNodes[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Worst case: O(N * K * (N*K)) = O(N^2 * K^2) when all derivatives unique
|
||||
```
|
||||
|
||||
## Fix
|
||||
|
||||
Use a `Set` for O(1) membership. For DOM Node dedup, `Set<Node>` is keyed by
|
||||
object identity, which matches the existing `indexOf` semantics exactly.
|
||||
|
||||
```javascript
|
||||
// filterNodes: build a Set from matching NodeList once
|
||||
const matchingSet = new Set();
|
||||
for (let i = 0; i < matching.length; i++)
|
||||
matchingSet.add(matching[i]);
|
||||
filter = node => matchingSet.has(node); // O(1)
|
||||
|
||||
// expandSelectorResults: parallel seen-Set tracks which nodes have been pushed
|
||||
const seen = new Set();
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const derivativeNodes = populateDerivativeNodes(nodes[i]);
|
||||
if (derivativeNodes) {
|
||||
for (let j = 0; j < derivativeNodes.length; j++) {
|
||||
if (!seen.has(derivativeNodes[j])) {
|
||||
seen.add(derivativeNodes[j]);
|
||||
result.push(derivativeNodes[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Cost drops to O(N+M) for filterNodes and O(N×K) for expandSelectorResults.
|
||||
|
||||
## Severity Note
|
||||
|
||||
Client-side code injected into the test page, called per-selector per-filter.
|
||||
Long test suites and selector-heavy pages compound the cost. Impact most
|
||||
visible on DOM tests against large data grids, dashboards, and virtualized
|
||||
lists.
|
||||
|
||||
## Complexity Gate
|
||||
|
||||
- filterNodes N=M=1000: fixed must complete in <5ms
|
||||
- expandSelectorResults N=K=100: fixed must complete in <5ms
|
||||
- k-scaling 5×: time ratio must be <17.5× (O(k) ≈5×, not O(k²) ≈25×)
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
# 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
|
||||
|
||||
```typescript
|
||||
// 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.
|
||||
|
||||
```typescript
|
||||
// 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×)
|
||||
Loading…
Add table
Add a link
Reference in a new issue