selenium-0001: SessionCapabilitiesMutator list.contains O(NxM) -> LinkedHashSet O(N+M). Grid Node session mutation hot path. Bench: 192x at N=M=1000. selenium-0002: ChromiumOptions merge helpers consolidate four list.contains loops behind addArgumentsUnique/addEncodedExtensionsUnique. Bench: 254x at N=M=1000. playwright-0001: roleUtils validRoles / allowsNameFromContent Array.includes on 20-70 element constant arrays per element. Converted to Set<string> at module load. Bench: 11x at N=10000 elements. webdriverio-0001: xpath-conditions extractOrConditions orMatches.find + values.includes per regex match -> Map<attr, Set<values>>. Bench: 6x at K=V=60 in the 'mobileSelectorPerformanceOptimizer'. Each defect ships: ticket, patch with complexity-gate header, Python benchmark + correctness test, Makefile, outreach brief. All 16 tests pass. UNDF IDs: 1276 (playwright), 1277 (selenium-0001), 1288 (selenium-0002), 1289 (webdriverio).
80 lines
5 KiB
Diff
80 lines
5 KiB
Diff
# UNDF: UNDF-2026-000001276
|
|
# UNDF: UNDF-2026-XXXXXXXXX
|
|
# CWE-407: Algorithmic Complexity -- O(N*k) -> O(N+k) in ARIA snapshot hot paths
|
|
#
|
|
# Defect: roleUtils.ts runs per-element during ARIA tree walks. Three helpers
|
|
# (getExplicitAriaRole, allowsNameFromContent, hasGlobalAriaAttribute) each
|
|
# call Array.includes on constant arrays of 20-70 role strings. For a page
|
|
# with N=5000 elements and k=70 roles, total cost is O(N*k) = 350,000
|
|
# string comparisons per snapshot.
|
|
#
|
|
# Fix: Convert the hot-path constant arrays to Set<string> at module scope.
|
|
# Per-element cost drops from O(k) to O(1). Keeps the original arrays as
|
|
# source-of-truth for readability; builds the Sets once.
|
|
#
|
|
# Complexity gate (tests/test-playwright-cwe407.py):
|
|
# k-scaling 5x: time ratio must be <17.5x (O(k) ~=5x, not O(k^2) ~=25x)
|
|
# N=5000 elements snapshot: must complete in <10ms
|
|
--- a/packages/injected/src/roleUtils.ts
|
|
+++ b/packages/injected/src/roleUtils.ts
|
|
@@ -56,11 +56,14 @@ const kGlobalAriaAttributes: [string, string[] | undefined][] = [
|
|
['aria-roledescription', ['generic']],
|
|
];
|
|
|
|
+// Pre-built Set views of the prohibited-role lists so hasGlobalAriaAttribute can do
|
|
+// O(1) membership lookup per attribute check instead of scanning the prohibited array
|
|
+// on every element during ARIA snapshot walks.
|
|
+const kGlobalAriaAttributeProhibitedSets: [string, Set<string> | undefined][] =
|
|
+ kGlobalAriaAttributes.map(([attr, prohibited]) => [attr, prohibited ? new Set(prohibited) : undefined]);
|
|
+
|
|
function hasGlobalAriaAttribute(element: Element, forRole?: string | null) {
|
|
- return kGlobalAriaAttributes.some(([attr, prohibited]) => {
|
|
- return !prohibited?.includes(forRole || '') && element.hasAttribute(attr);
|
|
- });
|
|
+ return kGlobalAriaAttributeProhibitedSets.some(([attr, prohibited]) => {
|
|
+ return !prohibited?.has(forRole || '') && element.hasAttribute(attr);
|
|
+ });
|
|
}
|
|
|
|
function hasTabIndex(element: Element) {
|
|
@@ -265,10 +268,13 @@ const validRoles: AriaRole[] = ['alert', 'alertdialog', 'application', 'article'
|
|
'spinbutton', 'status', 'strong', 'subscript', 'superscript', 'switch', 'tab', 'table', 'tablist', 'tabpanel', 'term', 'textbox', 'time', 'timer',
|
|
'toolbar', 'tooltip', 'tree', 'treegrid', 'treeitem'];
|
|
|
|
+// Set view of validRoles so getExplicitAriaRole can test role membership in O(1)
|
|
+// instead of scanning the 70+ element array per element during snapshot walks.
|
|
+const validRolesSet = new Set<string>(validRoles);
|
|
+
|
|
function getExplicitAriaRole(element: Element): AriaRole | null {
|
|
// https://www.w3.org/TR/wai-aria-1.2/#document-handling_author-errors_roles
|
|
const roles = (element.getAttribute('role') || '').split(' ').map(role => role.trim());
|
|
- return roles.find(role => validRoles.includes(role as any)) as AriaRole || null;
|
|
+ return roles.find(role => validRolesSet.has(role)) as AriaRole || null;
|
|
}
|
|
|
|
function hasPresentationConflictResolution(element: Element, role: string | null) {
|
|
@@ -496,12 +502,21 @@ function allowsNameFromContent(role: string, targetDescendant: boolean) {
|
|
// See chromium implementation here:
|
|
// https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/modules/accessibility/ax_object.cc;l=6338;drc=3decef66bc4c08b142a19db9628e9efe68973e64;bpv=0;bpt=1
|
|
- const alwaysAllowsNameFromContent = ['button', 'cell', 'checkbox', 'columnheader', 'gridcell', 'heading', 'link', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'radio', 'row', 'rowheader', 'switch', 'tab', 'tooltip', 'treeitem'].includes(role);
|
|
- const descendantAllowsNameFromContent = targetDescendant && ['', 'caption', 'code', 'contentinfo', 'definition', 'deletion', 'emphasis', 'insertion', 'list', 'listitem', 'mark', 'none', 'paragraph', 'presentation', 'region', 'row', 'rowgroup', 'section', 'strong', 'subscript', 'superscript', 'table', 'term', 'time'].includes(role);
|
|
+ const alwaysAllowsNameFromContent = kAlwaysAllowsNameFromContentSet.has(role);
|
|
+ const descendantAllowsNameFromContent = targetDescendant && kDescendantAllowsNameFromContentSet.has(role);
|
|
return alwaysAllowsNameFromContent || descendantAllowsNameFromContent;
|
|
}
|
|
|
|
+// Set views of the allowsNameFromContent role lists. Built once at module load;
|
|
+// per-call membership test is O(1) vs the prior O(k) Array.includes scan.
|
|
+const kAlwaysAllowsNameFromContentSet = new Set<string>([
|
|
+ 'button', 'cell', 'checkbox', 'columnheader', 'gridcell', 'heading', 'link',
|
|
+ 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'radio', 'row',
|
|
+ 'rowheader', 'switch', 'tab', 'tooltip', 'treeitem']);
|
|
+const kDescendantAllowsNameFromContentSet = new Set<string>([
|
|
+ '', 'caption', 'code', 'contentinfo', 'definition', 'deletion', 'emphasis',
|
|
+ 'insertion', 'list', 'listitem', 'mark', 'none', 'paragraph', 'presentation',
|
|
+ 'region', 'row', 'rowgroup', 'section', 'strong', 'subscript', 'superscript',
|
|
+ 'table', 'term', 'time']);
|
|
+
|
|
export function getElementAccessibleName(element: Element, includeHidden: boolean): string {
|
|
const cache = (includeHidden ? cacheAccessibleNameHidden : cacheAccessibleName);
|
|
let accessibleName = cache?.get(element);
|