perf: presize result slice in getChildrenWithSiblingType

For the sibling types that collect every match (siblingAll,
siblingAllIncludingNonElements, siblingPrevAll, siblingNextAll), do a
cheap first-pass pointer walk to count the matches, then make the result
slice with that exact capacity. This removes the repeated append-driven
slice growth that dominated allocations on these hot traversal paths.

The Until cases are left untouched because counting would require running
the user predicate twice, and the single-result Next/Prev cases have
nothing to presize.

This drives Children/Contents/Siblings/Next*/Prev* through fewer
allocations:

                       allocs/op vs base    B/op vs base
    Siblings            -57.23%             -9.23%
    SiblingsFiltered    -54.49%             -8.94%
    NextAll             -59.82%            -10.04%
    NextAllFiltered     -55.83%             -9.65%
    PrevAll             -55.32%            -10.66%
    PrevAllFiltered     -52.53%            -10.40%
    ChildrenFiltered    -15.38%             -6.45%
    Contents             -7.69%             -1.73%
    geomean             -25.07%             -3.88%

The Until and single Next/Prev benchmarks are unchanged (bit-identical
allocs/op), confirming the count pre-pass only touches the collect-all
paths.

The quite verbose comment on top of the change is there so that future
generations won't waste time wondering why this weird loop is here.
This commit is contained in:
jvoisin
2026-05-29 23:45:29 +02:00
parent 7a46fb40e9
commit f4b3a3e2c4
+14
View File
@@ -657,6 +657,20 @@ func getChildrenWithSiblingType(parent *html.Node, st siblingType, skipNode *htm
}
}
// For the cases that collect every matching sibling, count them in a
// cheap pointer walk first so the result slice can be sized exactly,
// avoiding repeated slice growth. The Until cases are skipped (counting
// would require running the predicate twice) and so are the single-result
// Next/Prev cases.
switch st {
case siblingAll, siblingAllIncludingNonElements, siblingPrevAll, siblingNextAll:
n := 0
for c := iter(nil); c != nil; c = iter(c) {
n++
}
result = make([]*html.Node, 0, n)
}
for c := iter(nil); c != nil; c = iter(c) {
// If this is an ...Until case, test before append (returns true
// if the until condition is reached)