From f4b3a3e2c4d915b2587991dac4dd82b1aeab4bf2 Mon Sep 17 00:00:00 2001 From: jvoisin Date: Fri, 29 May 2026 23:45:29 +0200 Subject: [PATCH] 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. --- traversal.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/traversal.go b/traversal.go index c495eea..f8f38da 100644 --- a/traversal.go +++ b/traversal.go @@ -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)