Skip to content

Commit 2a1641a

Browse files
committed
[XPath] Optimize XPath evaluation, now all QT3 tests can be run in reasonable time.
16246 72.2% successes 0 0.0% skipped 4007 17.8% failures 2261 10.0% errors
1 parent 817c528 commit 2a1641a

File tree

3 files changed

+110
-41
lines changed

3 files changed

+110
-41
lines changed

bin/xpath_qt3tests.dart

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,14 @@ import 'package:xml/src/xpath/types/string.dart';
1515
import 'package:xml/xml.dart';
1616
import 'package:xml/xpath.dart';
1717

18+
/// URL of the official XPath and XQpery W3C test-suite.
1819
const githubRepository = 'https://github.com/w3c/qt3tests.git';
20+
21+
/// Path to the local catalog file.
1922
final catalogFile = File('.qt3tests/catalog.xml').absolute;
2023

21-
// These tests are skipped because they are extremely slow.
22-
const skippedTests = <String>{
23-
'cbcl-subsequence-010',
24-
'cbcl-subsequence-011',
25-
'cbcl-subsequence-012',
26-
'cbcl-subsequence-013',
27-
'cbcl-subsequence-014',
28-
};
24+
/// Test names that are skipped.
25+
const skippedTests = <String>{};
2926

3027
void main() {
3128
downloadAndUpdateTestData();
@@ -156,20 +153,27 @@ class TestCase {
156153
result.skipped++;
157154
return;
158155
}
156+
final stopwatch = Stopwatch()..start();
159157
try {
160158
_test();
161-
stdout.writeln(': OK');
159+
stopwatch.stop();
160+
stdout.writeln(': OK ${formatStopwatch(stopwatch)}');
162161
result.successes++;
163162
} on TestFailure catch (error) {
164-
stdout.writeln(': FAILURE - ${error.message}');
163+
stopwatch.stop();
164+
stdout.writeln(
165+
': FAILURE ${formatStopwatch(stopwatch)} - ${formatMessage(error.message)}',
166+
);
165167
result.failures++;
166168
} catch (error) {
167169
final message = error is StateError
168170
? error.message
169171
: error is UnsupportedError
170-
? error.message
172+
? (error.message ?? 'Unsupported')
171173
: error.toString();
172-
stdout.writeln(': ERROR - $message');
174+
stdout.writeln(
175+
': ERROR ${formatStopwatch(stopwatch)} - ${formatMessage(message)}',
176+
);
173177
result.errors++;
174178
}
175179
}
@@ -412,3 +416,13 @@ String formatSequence(XPathSequence sequence) =>
412416
return item.toString();
413417
}
414418
}).join(', ')})';
419+
420+
/// Helper to format a stopwatch duration.
421+
String formatStopwatch(Stopwatch stopwatch) =>
422+
'[${(stopwatch.elapsed.inMicroseconds / 1000).toStringAsFixed(3)}ms]';
423+
424+
/// Helper to format a error message.
425+
String formatMessage(String message) {
426+
final normalize = message.trim().replaceAll(RegExp(r'\s+'), ' ');
427+
return normalize.length > 80 ? '${normalize.substring(0, 75)}...' : normalize;
428+
}

lib/src/xpath/expressions/path.dart

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
import 'package:collection/collection.dart';
2+
13
import '../../xml/extensions/comparison.dart';
4+
import '../../xml/extensions/descendants.dart';
25
import '../../xml/extensions/parent.dart';
36
import '../../xml/nodes/node.dart';
47
import '../evaluation/context.dart';
@@ -194,14 +197,34 @@ class PathExpression implements XPathExpression {
194197
}
195198
nodes = innerNodes;
196199
}
197-
final result = nodes.toList();
198-
result.sort((a, b) {
199-
if (a is XmlNode && b is XmlNode) {
200-
return a.compareNodePosition(b);
201-
}
202-
return 0;
203-
});
204-
return XPathSequence(result);
200+
return XPathSequence(_sortAndDeduplicate(nodes, contextNode.root));
201+
}
202+
}
203+
204+
static List<Object> _sortAndDeduplicate(Iterable<Object> iter, XmlNode root) {
205+
final nodes = <XmlNode>{};
206+
final others = <Object>{};
207+
for (final item in iter) {
208+
if (item is XmlNode) {
209+
nodes.add(item);
210+
} else {
211+
others.add(item);
212+
}
213+
}
214+
final result = <Object>[];
215+
if (nodes.length <= 25) {
216+
result.addAll(nodes.sorted((a, b) => a.compareNodePosition(b)));
217+
} else {
218+
if (nodes.remove(root)) result.add(root);
219+
for (final node in root.descendants) {
220+
if (nodes.isEmpty) break;
221+
if (nodes.remove(node)) result.add(node);
222+
}
223+
if (nodes.isNotEmpty) {
224+
result.addAll(nodes.sorted((a, b) => a.compareNodePosition(b)));
225+
}
205226
}
227+
result.addAll(others);
228+
return result;
206229
}
207230
}

lib/src/xpath/functions/sequence.dart

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import 'package:collection/collection.dart';
2-
31
import '../../../xml.dart';
42
import '../definitions/cardinality.dart';
53
import '../definitions/function.dart';
@@ -244,34 +242,68 @@ const fnSubsequence = XPathFunctionDefinition(
244242
type: xsAny,
245243
cardinality: XPathCardinality.zeroOrMore,
246244
),
247-
XPathArgumentDefinition(name: 'startingLoc', type: xsInteger),
245+
XPathArgumentDefinition(name: 'startingLoc', type: xsDouble),
248246
],
249-
optionalArguments: [XPathArgumentDefinition(name: 'length', type: xsInteger)],
247+
optionalArguments: [XPathArgumentDefinition(name: 'length', type: xsDouble)],
250248
function: _fnSubsequence,
251249
);
252250

253251
XPathSequence _fnSubsequence(
254252
XPathContext context,
255253
XPathSequence sourceSeq,
256-
int startingLoc, [
257-
int? length,
254+
double startingLoc, [
255+
double? length,
258256
]) {
259-
final start = startingLoc;
260-
final len = length;
261-
if (len != null) {
262-
final end = start + len;
263-
return XPathSequence(
264-
sourceSeq.toList().whereIndexed((int index, Object item) {
265-
final pos = index + 1;
266-
return pos >= start && pos < end;
267-
}),
268-
);
257+
if (startingLoc.isNaN || (length != null && length.isNaN)) {
258+
return XPathSequence.empty;
269259
}
270-
return XPathSequence(
271-
sourceSeq.toList().whereIndexed(
272-
(int index, Object item) => index + 1 >= start,
273-
),
274-
);
260+
261+
final startRound = startingLoc.isInfinite
262+
? startingLoc
263+
: startingLoc.roundToDouble();
264+
265+
final lengthRound = length == null
266+
? null
267+
: (length.isInfinite ? length : length.roundToDouble());
268+
269+
final endRound = lengthRound != null
270+
? startRound + lengthRound
271+
: double.infinity;
272+
273+
if (endRound.isNaN ||
274+
endRound <= 1.0 ||
275+
(startRound.isInfinite && startRound > 0)) {
276+
return XPathSequence.empty;
277+
}
278+
279+
var skipCount = 0;
280+
if (startRound > 1.0) {
281+
if (startRound > 9007199254740992.0) {
282+
return XPathSequence.empty;
283+
}
284+
skipCount = (startRound - 1.0).toInt();
285+
}
286+
287+
int? takeCount;
288+
if (endRound != double.infinity) {
289+
if (endRound > 9007199254740992.0) {
290+
takeCount = null;
291+
} else {
292+
final take = (endRound - 1.0).toInt() - skipCount;
293+
if (take <= 0) return XPathSequence.empty;
294+
takeCount = take;
295+
}
296+
}
297+
298+
Iterable<Object> iter = sourceSeq;
299+
if (skipCount > 0) {
300+
iter = iter.skip(skipCount);
301+
}
302+
if (takeCount != null) {
303+
iter = iter.take(takeCount);
304+
}
305+
306+
return XPathSequence(iter);
275307
}
276308

277309
/// https://www.w3.org/TR/xpath-functions-31/#func-unordered

0 commit comments

Comments
 (0)