Skip to content

Commit f620cf4

Browse files
Optimization: @getodk/xpath is faster without Iterable/Iterator (#377)
1 parent ea9674c commit f620cf4

30 files changed

+245
-1020
lines changed

.changeset/funny-carrots-buy.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@getodk/xforms-engine': patch
3+
'@getodk/xpath': patch
4+
---
5+
6+
Improves XPath processing performance.

packages/xforms-engine/src/integration/xpath/adapter/traversal.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ export const getContainingEngineXPathDocument = (node: EngineXPathNode): EngineX
1818
return node.rootDocument;
1919
};
2020

21-
export const getEngineXPathAttributes = (node: EngineXPathNode): Iterable<EngineXPathAttribute> => {
21+
export const getEngineXPathAttributes = (
22+
node: EngineXPathNode
23+
): readonly EngineXPathAttribute[] => {
2224
if (node.nodeType === 'static-element') {
2325
return node.attributes;
2426
}
@@ -39,7 +41,7 @@ export const getEngineXPathAttributes = (node: EngineXPathNode): Iterable<Engine
3941
* it throw? It might be nice to be alerted if the assumptions in point 2 above
4042
* are somehow wrong (or become wrong).
4143
*/
42-
export const getNamespaceDeclarations = (): Iterable<never> => [];
44+
export const getNamespaceDeclarations = (): readonly [] => [];
4345

4446
export const getParentNode = (node: EngineXPathNode): EngineXPathParentNode | null => {
4547
if (node.nodeType === 'repeat-instance') {

packages/xpath/package.json

-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@
5656
"dependencies": {
5757
"@getodk/common": "0.5.2",
5858
"crypto-js": "^4.2.0",
59-
"itertools-ts": "^2.2.0",
6059
"temporal-polyfill": "^0.3.0"
6160
},
6261
"devDependencies": {

packages/xpath/src/adapter/interface/XPathDOMOptimizableOperations.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export interface XPathDOMOptimizableOperations<T extends XPathNode> {
2525
readonly getChildrenByLocalName: (
2626
node: AdapterParentNode<T>,
2727
localName: string
28-
) => Iterable<AdapterElement<T>>;
28+
) => ReadonlyArray<AdapterElement<T>>;
2929

3030
readonly getFirstChildNode: (node: T) => AdapterChildNode<T> | null;
3131
readonly getFirstChildElement: (node: T) => AdapterElement<T> | null;

packages/xpath/src/adapter/interface/XPathTraversalAdapter.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ export interface XPathTraversalAdapter<T extends XPathNode> {
2222
*/
2323
readonly getContainingDocument: (node: T) => AdapterDocument<T>;
2424

25-
readonly getNamespaceDeclarations: (node: T) => Iterable<AdapterNamespaceDeclaration<T>>;
26-
readonly getAttributes: (node: T) => Iterable<AdapterAttribute<T>>;
25+
readonly getNamespaceDeclarations: (node: T) => ReadonlyArray<AdapterNamespaceDeclaration<T>>;
26+
readonly getAttributes: (node: T) => ReadonlyArray<AdapterAttribute<T>>;
2727
readonly getParentNode: (node: T) => AdapterParentNode<T> | null;
28-
readonly getChildNodes: (node: T) => Iterable<AdapterChildNode<T>>;
29-
readonly getChildElements: (node: T) => Iterable<AdapterElement<T>>;
28+
readonly getChildNodes: (node: T) => ReadonlyArray<AdapterChildNode<T>>;
29+
readonly getChildElements: (node: T) => ReadonlyArray<AdapterElement<T>>;
3030
readonly getPreviousSiblingNode: (node: T) => AdapterChildNode<T> | null;
3131
readonly getPreviousSiblingElement: (node: T) => AdapterElement<T> | null;
3232
readonly getNextSiblingNode: (node: T) => AdapterChildNode<T> | null;

packages/xpath/src/adapter/xpathDOMProvider.ts

+17-70
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { UpsertableMap } from '@getodk/common/lib/collections/UpsertableMap.ts';
2-
import { filter } from '../lib/iterators/common.ts';
32
import type { XPathDOMAdapter } from './interface/XPathDOMAdapter.ts';
43
import type { XPathDOMOptimizableOperations } from './interface/XPathDOMOptimizableOperations.ts';
54
import type { XPathNode } from './interface/XPathNode.ts';
@@ -118,53 +117,6 @@ const extendNodeKindGuards = <T extends XPathNode>(
118117
return Object.assign(base, extensions);
119118
};
120119

121-
type IterableNodeFilter<T extends XPathNode, U extends T> = (nodes: Iterable<T>) => Iterable<U>;
122-
123-
/**
124-
* Provides frequently used operations, such as filtering and sorting, on
125-
* {@link Iterable} sequences of an {@link XPathDOMAdapter}'s node
126-
* representation.
127-
*/
128-
interface IterableOperations<T extends XPathNode> {
129-
readonly filterAttributes: IterableNodeFilter<T, AdapterAttribute<T>>;
130-
readonly filterQualifiedNamedNodes: IterableNodeFilter<T, AdapterQualifiedNamedNode<T>>;
131-
readonly filterComments: IterableNodeFilter<T, AdapterComment<T>>;
132-
readonly filterNamespaceDeclarations: IterableNodeFilter<T, AdapterNamespaceDeclaration<T>>;
133-
readonly filterProcessingInstructions: IterableNodeFilter<T, AdapterProcessingInstruction<T>>;
134-
readonly filterTextNodes: IterableNodeFilter<T, AdapterText<T>>;
135-
136-
// Note: iterable -> array is intentional. Can't sort a lazy, arbitrary-order
137-
// sequence without iterating every item!
138-
readonly sortInDocumentOrder: (nodes: Iterable<T>) => readonly T[];
139-
}
140-
141-
interface ExtendedIterableOperations<T extends XPathNode>
142-
extends ExtendedNodeKindGuards<T>,
143-
IterableOperations<T> {}
144-
145-
/**
146-
* Derives frequently used {@link IterableOperations | iterable operations} from an
147-
* {@link XPathDOMAdapter} and its derived
148-
* {@link NodeKindGuards | node kind predicates}.
149-
*/
150-
const extendIterableOperations = <T extends XPathNode>(
151-
base: ExtendedNodeKindGuards<T>
152-
): ExtendedIterableOperations<T> => {
153-
const extensions: IterableOperations<T> = {
154-
filterAttributes: filter(base.isAttribute),
155-
filterQualifiedNamedNodes: filter(base.isQualifiedNamedNode),
156-
filterComments: filter(base.isComment),
157-
filterNamespaceDeclarations: filter(base.isNamespaceDeclaration),
158-
filterProcessingInstructions: filter(base.isProcessingInstruction),
159-
filterTextNodes: filter(base.isText),
160-
sortInDocumentOrder: (nodes: Iterable<T>): readonly T[] => {
161-
return Array.from(nodes).sort((a, b) => base.compareDocumentOrder(a, b));
162-
},
163-
};
164-
165-
return Object.assign(base, extensions);
166-
};
167-
168120
type UniqueIDElementLookup<T extends XPathNode> = (
169121
node: AdapterDocument<T>,
170122
id: string
@@ -180,29 +132,26 @@ const getElementByUniqueIdFactory = <T extends XPathNode>(
180132
return adapterImplementation;
181133
}
182134

183-
function* getElementDescendants(node: AdapterParentNode<T>): Iterable<AdapterElement<T>> {
184-
if (adapter.isElement(node)) {
185-
yield node;
186-
}
187-
188-
for (const element of adapter.getChildElements(node)) {
189-
yield element;
190-
191-
yield* getElementDescendants(element);
135+
const getElementByUniqueId = (
136+
node: AdapterParentNode<T>,
137+
id: string
138+
): AdapterElement<T> | null => {
139+
if (adapter.isElement(node) && getNamedAttributeValue(node, 'id') === id) {
140+
return node;
192141
}
193-
}
194142

195-
return (node, id) => {
196-
const containingDocument = adapter.getContainingDocument(node);
143+
for (const childElement of adapter.getChildElements(node)) {
144+
const element = getElementByUniqueId(childElement, id);
197145

198-
for (const element of getElementDescendants(containingDocument)) {
199-
if (getNamedAttributeValue(element, 'id') === id) {
146+
if (element != null) {
200147
return element;
201148
}
202149
}
203150

204151
return null;
205152
};
153+
154+
return getElementByUniqueId;
206155
};
207156

208157
type QualifiedNamedAttributeValueLookup<T extends XPathNode> = (
@@ -286,7 +235,7 @@ const hasLocalNamedAttributeFactory = <T extends XPathNode>(
286235
type LocalNamedChildElementsLookup<T extends XPathNode> = (
287236
node: AdapterParentNode<T>,
288237
localName: string
289-
) => Iterable<AdapterElement<T>>;
238+
) => ReadonlyArray<AdapterElement<T>>;
290239

291240
const getChildrenByLocalNameFactory = <T extends XPathNode>(
292241
adapter: XPathDOMAdapter<T>
@@ -298,7 +247,7 @@ const getChildrenByLocalNameFactory = <T extends XPathNode>(
298247
}
299248

300249
return (node, localName) => {
301-
return Array.from(adapter.getChildElements(node)).filter((element) => {
250+
return adapter.getChildElements(node).filter((element) => {
302251
return adapter.getLocalName(element) === localName;
303252
});
304253
};
@@ -364,7 +313,7 @@ const getLastChildElementFactory = <T extends XPathNode>(
364313
}
365314

366315
return (node) => {
367-
return Array.from(adapter.getChildElements(node)).at(-1) ?? null;
316+
return adapter.getChildElements(node).at(-1) ?? null;
368317
};
369318
};
370319

@@ -376,7 +325,7 @@ const getLastChildElementFactory = <T extends XPathNode>(
376325
type OmitOptionalOptimizableOperations<T> = Omit<T, keyof XPathDOMOptimizableOperations<XPathNode>>;
377326

378327
interface ExtendedOptimizableOperations<T extends XPathNode>
379-
extends OmitOptionalOptimizableOperations<ExtendedIterableOperations<T>>,
328+
extends OmitOptionalOptimizableOperations<ExtendedNodeKindGuards<T>>,
380329
XPathDOMOptimizableOperations<T> {}
381330

382331
/**
@@ -391,7 +340,7 @@ interface ExtendedOptimizableOperations<T extends XPathNode>
391340
* will be derived from other aspects of the adapter's required APIs.
392341
*/
393342
const extendOptimizableOperations = <T extends XPathNode>(
394-
base: ExtendedIterableOperations<T>
343+
base: ExtendedNodeKindGuards<T>
395344
): ExtendedOptimizableOperations<T> => {
396345
const getLocalNamedAttributeValue = getLocalNamedAttributeValueFactory(base);
397346

@@ -451,7 +400,6 @@ const derivedDOMProvider = <T>(base: T): DerivedDOMProvider & T => {
451400
export interface XPathDOMProvider<T extends XPathNode>
452401
extends OmitOptionalOptimizableOperations<XPathDOMAdapter<T>>,
453402
NodeKindGuards<T>,
454-
IterableOperations<T>,
455403
XPathDOMOptimizableOperations<T>,
456404
DerivedDOMProvider {}
457405

@@ -477,8 +425,7 @@ export const xpathDOMProvider = <T extends XPathNode>(
477425
}
478426

479427
const extendedGuards = extendNodeKindGuards(adapter);
480-
const extendedIterableOperations = extendIterableOperations(extendedGuards);
481-
const exended = extendOptimizableOperations(extendedIterableOperations);
428+
const exended = extendOptimizableOperations(extendedGuards);
482429

483430
return derivedDOMProvider(exended);
484431
};

packages/xpath/src/context/Context.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export interface Context<T extends XPathNode> {
3030

3131
readonly contextDocument: AdapterDocument<T>;
3232
readonly rootNode: AdapterParentNode<T>;
33-
readonly contextNodes: Iterable<T>;
33+
readonly contextNodes: ReadonlySet<T>;
3434

3535
contextPosition(): number;
3636
contextSize(): number;

packages/xpath/src/context/EvaluationContext.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export class EvaluationContext<T extends XPathNode> implements Context<T> {
3333
readonly contextDocument: AdapterDocument<T>;
3434
readonly rootNode: AdapterParentNode<T>;
3535

36-
readonly contextNodes: Iterable<T>;
36+
readonly contextNodes: ReadonlySet<T>;
3737

3838
readonly functions: FunctionLibraryCollection;
3939
readonly namespaceResolver: NamespaceResolver<T>;
@@ -56,7 +56,7 @@ export class EvaluationContext<T extends XPathNode> implements Context<T> {
5656

5757
this.contextDocument = contextDocument;
5858
this.evaluationContextNode = contextNode;
59-
this.contextNodes = [contextNode];
59+
this.contextNodes = new Set([contextNode]);
6060
this.rootNode = rootNode;
6161
this.functions = options.functions ?? evaluator.functions;
6262
this.namespaceResolver = NamespaceResolver.from(

packages/xpath/src/evaluations/Evaluation.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export interface Evaluation<T extends XPathNode, Type extends EvaluationType = E
88
readonly type: Type;
99

1010
first(): Evaluation<T, Type> | null;
11-
values(): Iterable<Evaluation<T, Type>>;
11+
values(): ReadonlyArray<Evaluation<T, Type>>;
1212

1313
eq(operand: Evaluation<T>): boolean;
1414
ne(operand: Evaluation<T>): boolean;
@@ -21,5 +21,5 @@ export interface Evaluation<T extends XPathNode, Type extends EvaluationType = E
2121
toNumber(): number;
2222
toString(): string;
2323

24-
readonly nodes: Type extends 'NODE' ? Iterable<T> : null;
24+
readonly nodes: Type extends 'NODE' ? ReadonlySet<T> : null;
2525
}

0 commit comments

Comments
 (0)