Skip to content

Commit 6a3d679

Browse files
committed
perf(xslt): bound key duplicate checks
1 parent 6fb5672 commit 6a3d679

6 files changed

Lines changed: 92 additions & 4 deletions

File tree

docs/changelog/799.feature.25.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Avoid repeated duplicate scans when :class:`~turbohtml.transform.Transform` indexes XSLT keys.

src/turbohtml/_c/query/xslt.c

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -320,10 +320,9 @@ typedef struct {
320320
} nodevec;
321321

322322
static int nodevec_push(nodevec *vec, th_node *node) {
323-
for (Py_ssize_t index = 0; index < vec->len; index++) {
324-
if (vec->nodes[index] == node) {
325-
return 0;
326-
}
323+
/* build_key processes each source node's values together in document order. */
324+
if (vec->len > 0 && vec->nodes[vec->len - 1] == node) {
325+
return 0;
327326
}
328327
if (vec->len == vec->cap) {
329328
Py_ssize_t cap = vec->cap == 0 ? 4 : vec->cap * 2;

tests/convert/test_transform.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,62 @@ def test_transform_key_deduplicates_a_node_under_one_value() -> None:
792792
assert _run("<r><i><t>same</t><t>same</t></i></r>", body) == "1"
793793

794794

795+
@pytest.mark.parametrize(
796+
("match", "use", "source", "wanted", "expected"),
797+
[
798+
pytest.param("i", "'same'", '<r><i id="a"/><i id="b"/><i id="c"/></r>', "same", "abc", id="scalar-key"),
799+
pytest.param(
800+
"i",
801+
"t",
802+
'<r><i id="a"><t>x</t><t>y</t><t>x</t></i><i id="b"><t>x</t><t>x</t></i></r>',
803+
"x",
804+
"ab",
805+
id="interleaved-use-values",
806+
),
807+
pytest.param(
808+
"i",
809+
"@a | @b",
810+
'<r><i id="a" a="x" b="x"/><i id="b" a="x" b="y"/></r>',
811+
"x",
812+
"ab",
813+
id="duplicate-attribute-values",
814+
),
815+
pytest.param(
816+
"/r/i | /r/i/@a | /r/i/@b",
817+
"'same'",
818+
'<r><i id="a" a="x" b="y"/><i id="b" a="x" b="y"/></r>',
819+
"same",
820+
"ab",
821+
id="element-and-attribute-match-owners",
822+
),
823+
pytest.param(
824+
"id('c a b a')",
825+
"'same'",
826+
'<r><i id="a"/><i id="b"/><i id="c"/></r>',
827+
"same",
828+
"abc",
829+
id="id-pattern-order-and-duplicates",
830+
),
831+
pytest.param("i", "t", '<r><i id="a"/><i id="b"><t>x</t></i></r>', "x", "b", id="empty-use-node-set"),
832+
pytest.param(
833+
"i",
834+
"t",
835+
'<r><i id="a"><t/></i><i id="b"><t/><t/></i></r>',
836+
"",
837+
"ab",
838+
id="empty-key-string",
839+
),
840+
],
841+
)
842+
def test_transform_key_bucket_duplicate_order(match: str, use: str, source: str, wanted: str, expected: str) -> None:
843+
body: Final = (
844+
f'<xsl:key name="k" match="{match}" use="{use}"/>'
845+
f'<xsl:template match="/"><xsl:for-each select="key(&quot;k&quot;,&quot;{wanted}&quot;)">'
846+
'<xsl:value-of select="@id"/></xsl:for-each></xsl:template>'
847+
)
848+
assert _run(source, body) == expected
849+
850+
795851
def test_transform_key_string_use_expression() -> None:
796852
body = (
797853
'<xsl:key name="k" match="i" use="string(@n)"/>'

tools/bench/ci.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,11 @@ def _book_html() -> str:
429429
"normalize-marks-large": ("normalize-marks", 2),
430430
"normalize-marks-small": ("normalize-marks", 0),
431431
"normalize-hebrew-marks": ("normalize", 8),
432+
"transform-key": ("transform-key", 0),
433+
"transform-key-attribute": ("transform-key", 1),
434+
"transform-key-repeated": ("transform-key", 2),
435+
"transform-key-unique": ("transform-key", 3),
436+
"transform-key-small": ("transform-key", 4),
432437
}
433438

434439

tools/bench/core.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1704,6 +1704,7 @@ def _query_root_group_case(case: tuple[str, int]) -> _Query:
17041704
"transform-names-compile": (transform_compile, "turbohtml"),
17051705
"transform-reuse": (transform_reuse, "turbohtml"),
17061706
"transform-sort": (transform, "turbohtml"),
1707+
"transform-key": (transform, "turbohtml"),
17071708
"transform-dense": (transform, "turbohtml"),
17081709
"transform-number": (transform, "turbohtml"),
17091710
"transform-rules": (transform, "turbohtml"),

tools/bench/operations.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,7 @@ class Operation:
494494
"transform-compile": Operation("compile an XSLT stylesheet with 300 templates", "us"),
495495
"transform-reuse": Operation("apply one compiled 300-template stylesheet ten times", "us"),
496496
"transform-sort": Operation("XSLT sort node sets", "ms"),
497+
"transform-key": Operation("build XSLT key indexes", "us"),
497498
"transform-dense": Operation("XSLT transform an instruction-dense sheet", "us"),
498499
"transform-names-compile": Operation("compile XSLT declaration indexes", "us"),
499500
"transform-names": Operation("resolve XSLT declaration names", "us"),
@@ -1043,6 +1044,30 @@ def _transform_text_cases() -> tuple[tuple[str, object], ...]:
10431044
return tuple(cases)
10441045

10451046

1047+
def _transform_key_cases() -> tuple[tuple[str, object], ...]:
1048+
return tuple(
1049+
(
1050+
f"{label} ({count:,} nodes)",
1051+
(
1052+
(
1053+
'<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">'
1054+
f'<xsl:key name="k" match="i" use="{use}"/><xsl:output method="text"/>'
1055+
f'<xsl:template match="/"><xsl:value-of select="count(key(&quot;k&quot;,&quot;{wanted}&quot;))"/>'
1056+
"</xsl:template></xsl:stylesheet>"
1057+
),
1058+
"<r>" + "".join(item.format(index=index) for index in range(count)) + "</r>",
1059+
),
1060+
)
1061+
for label, use, wanted, item, count in (
1062+
("shared scalar key", "'same'", "same", "<i/>", 8192),
1063+
("shared attribute key", "@key", "same", '<i key="same"/>', 8192),
1064+
("repeated node-set keys", "t", "same", "<i><t>same</t><t>other</t><t>same</t></i>", 2048),
1065+
("unique keys", "@key", "k0", '<i key="k{index}"/>', 8192),
1066+
("small shared key", "'same'", "same", "<i/>", 4),
1067+
)
1068+
)
1069+
1070+
10461071
def _transform_cases() -> tuple[tuple[str, object], ...]:
10471072
"""Return the one XSLT case: a real stylesheet (sort, key, number, format-number) over a 120-row catalog."""
10481073
return (("catalog (120 rows)", (_XSLT_SHEET, _XSLT_SOURCE)),)
@@ -2401,6 +2426,7 @@ def _encoding_result_cases() -> tuple[tuple[str, bytes], ...]:
24012426
"transform-compile": _transform_compile_cases,
24022427
"transform-reuse": _transform_compile_cases,
24032428
"transform-sort": _transform_sort_cases,
2429+
"transform-key": _transform_key_cases,
24042430
"transform-dense": _transform_dense_cases,
24052431
"transform-names": _transform_name_cases,
24062432
"transform-names-compile": lambda: (_transform_name_cases()[0],),

0 commit comments

Comments
 (0)