33 Commits
Author SHA1 Message Date
jvoisin 36f7b3b68e cleanup: thread []*html.Node through winnow, drop temp Selection
winnow only ever reads sel.Nodes; the *Selection parameter was
incidental. Change its signature to winnow(nodes []*html.Node, ...)
and update its three callers (FilterMatcher, NotMatcher,
filterAndPush). filterAndPush no longer needs to fabricate a
throwaway Selection to satisfy the call.

While this change has o measurable runtime effect, it simplifies the code a bit,
by making winnow's signature clearer, and removes a now-useless line and its
associated comment in filterAndPush
2026-06-05 22:35:48 +02:00
jvoisin df88dce547 Refactor first-match optimization into singleMatcher
Move the "stop at first match" optimization out of HasMatcher and into
singleMatcher, where it can be reused elsewhere.

singleMatcher now implements MatchFirst, which calls the underlying
Matcher's MatchFirst if it provides one (cascadia-compiled matchers all
do), otherwise falls back to MatchAll and returns its first element.
singleMatcher.MatchAll wraps MatchFirst's result in a single-item slice
to respect the Matcher interface, so external SingleMatcher() users get
the slice signature while internal callers can avoid that allocation.

HasMatcher now constructs a singleMatcher directly to call MatchFirst
without a type assertion, removing the duplicated probe branch.
2026-06-02 14:15:45 +02:00
jvoisin 021fb11910 perf: probe with MatchFirst in HasMatcher to avoid slice allocation
HasMatcher only needs to know whether a child subtree contains a match,
but it tested len(m.MatchAll(c)) > 0 after wrapping the matcher with
SingleMatcher. SingleMatcher.MatchAll computes MatchFirst internally and
then wraps the result in a throwaway []*html.Node{node}, allocating a
one-element slice per matching child subtree just to check a boolean.

When the matcher exposes MatchFirst, probe it directly and test the returned
node against nil, avoiding the allocation entirely. This is the same trick
already used in SingleMatcher.MatchAll, just applied at the HasMatcher call
site so the intermediate slice is never built in the first place. The
SingleMatcher path is kept as a fallback for matchers that don't implement
MatchFirst.

benchstat (count=10):

      | before       |              after   |
      | sec/op       |    sec/op     vs base|
Has-8 | 2.675µ ± 17% | 2.208µ ± 17%  -17.47%|

      | before       |             after    |
      |  B/op        |    B/op     vs base  |
Has-8 | 360.0 ± 0%   | 240.0 ± 0%  -33.33%  |

      | before       |             after    |
      |allocs/op     | allocs/op   vs base  |
Has-8 | 20.00 ± 0%   |  6.000 ± 0%  -70.00% |
2026-05-30 23:18:55 +02:00
jvoisin 9082a116aa perf: probe subtrees directly in Has / HasMatcher
Has(selector) and HasMatcher(m) used to call
s.document.Find(selector)/FindMatcher(m), materializing every matching
descendant in the entire document, then iterate s.Nodes x matchedNodes x
treeDepth via nodeContains to keep the elements that contain one.

This commit probes each selection node's subtree directly with SingleMatcher, which
short-circuits on the first descendant match.

BenchmarkHas (DocW().Find("h2").Has(".editsection")), -count=10:

               sec/op       B/op
    before     61.97µs       744 B
    after       3.33µs       360 B
                -94.6%      -51.6%
2026-05-29 17:19:03 +02:00
Martin Angers bd4f51414c Merge branch 'master' into precise 2026-05-28 10:21:55 -04:00
Martin Angers d260d2936f Merge pull request #558 from jvoisin/has-nodes-direct
perf: walk s.Nodes directly in HasNodes
2026-05-28 10:15:26 -04:00
jvoisin 5f07eebb27 perf: presize node-set maps in mapNodes, ClosestNodes and winnowNodes
Three internal helpers build a map[*html.Node]bool from a known set of
nodes (or use one as a dedup accumulator over a known number of input
nodes) but allocate the map without a size hint. They then incur
grow-and-double rehashing as entries are inserted.

Pass len(nodes) as the size hint at make time:

- traversal.go mapNodes: dedup map shared by appendWithoutDuplicates
  across every per-node result. mapNodes is the engine behind most
  traversal helpers (Parents, Children, Next/Prev, Siblings, Find*,
  Contents, ...), so the impact is broad.
- traversal.go ClosestNodes: target-node set built from nodes...
- filter.go winnowNodes: large-N path's lookup set built from nodes...

Double-digit performance gains across ~all benchmarks.
2026-05-27 23:11:36 +02:00
jvoisin bfc9c5940d perf: walk s.Nodes directly in HasNodes
HasNodes routed through FilterFunction -> winnowFunction -> grep, which
allocated a single-node Selection and a closure capture per source element,
just to call sel.Contains(n) which is itself a one-iteration sliceContains over
the wrapped node. This commit inlines a direct double loop over s.Nodes x nodes
calling nodeContains, and push the result via pushStack.

name             old allocs/op  new allocs/op  delta
Has-8                  55.00 ± 0%     24.00 ± 0%  -56.36% (p=0.000)
HasNodes-8            752.00 ± 0%      4.00 ± 0%  -99.47% (p=0.000)
HasSelection-8        752.00 ± 0%      3.00 ± 0%  -99.60% (p=0.000)
2026-05-27 12:11:29 +02:00
jvoisin 8ea13ea017 perf: drop grep/closure indirection in winnowNodes
In the same spirit as #545, replaces costly grep() with a loop.

While escape analysis managed to stack-allocate the per-node
Selection through grep's inlining, the closure capture itself (nodes and keep)
was still heap-allocated. Local benchmarks are showing a 30% allocs/op
reduction, and a ~10-20% reduction in sec/op.
2026-05-27 11:52:43 +02:00
jvoisin 45d35d506a perf: avoid per-node Selection allocation in winnow Not path
The winnow() "Not" path routed through grep(), which called
newSingleSelection() for every node, allocating a Selection and
a 1-element []*html.Node slice on the heap per node, only to
immediately unwrap it via s.Get(0) for m.Match().
This commit call m.Match(n) directly and pre-allocate the result slice.

It also adds BenchmarkNotMatcher to isolate the winnow path from
compileMatcher overhead.

Here are my local benchmark results, for BenchmarkNotMatcher with 373 <li>
nodes, and a pre-compiled matcher:

old ns/op   new ns/op   delta
22750       19350       ~-15%

old B/op    new B/op    delta
9384        3120        -66.7%

old allocs  new allocs  delta
11          2           -81.8%
2026-05-23 22:02:15 +02:00
Martin Angers 5dccaa115b optimize winnowNodes and ClosestNodes in the same way 2016-08-28 11:55:36 -04:00
Martin Angers 2ab590df05 use a failing matcher if selector fails to compile, adjust tests 2016-06-15 09:27:17 -04:00
Martin Angers 9cc531274b implement tests for selection methods with invalid selector strings 2016-06-15 08:42:27 -04:00
HARUYAMA Seigo d9b2b5ed51 cascadia has moved.
code.google.com/p/cascadia
->
github.com/andybalholm/cascadia
2015-03-28 11:15:11 +09:00
Martin Angers 875b65f385 finish transition for Matcher-based overloads 2014-11-07 08:36:55 -05:00
Martin Angers 251095afb5 start adding matcher functions 2014-11-06 21:43:08 -05:00
Martin Angers cc9b6f8c6f Merge branch 'master' into manipulation 2014-11-06 20:46:11 -05:00
Martin Angers 040cf98281 change paths to golang.org/x/, fails to build because cascadia has to switch too 2014-11-05 20:30:33 -05:00
Andrew Stone 9848519841 filter: Add *Selector for filtering methods that accepts a pre-compiled cascadia selector. 2014-10-24 11:49:05 -04:00
Martin Angers 28cf4a77e1 more linting 2014-04-11 12:04:03 -04:00
Martin Angers 2c6687f6d3 lint 2014-04-11 12:00:17 -04:00
Dobrosław Żybort 474b169363 Update exp/html import paths 2013-02-12 20:32:16 +01:00
Martin Angers 6669ef001f remove Document.Root, Document is itself a Selection (using embedded struct) 2012-10-14 12:28:27 -04:00
Martin Angers 6e1e76af6b refactor Document to make Root a Selection, with all API available 2012-09-02 14:45:50 -04:00
Martin Angers a139b5d77f doc fixes 2012-08-31 10:47:28 -04:00
Martin Angers f1e54e5311 add Add() and AndSelf(), with tests 2012-08-31 10:27:45 -04:00
Martin Angers 437a1dc2a1 work on filter.go, needs tests now 2012-08-30 22:47:12 -04:00
Martin Angers e4467e0f04 panic on error, instead of empty selection (like jQuery/Sizzle) 2012-08-30 15:08:36 -04:00
Martin Angers 2db07845a9 rework some internals, similar to jQuery 2012-08-30 14:36:36 -04:00
Martin Angers c3f897ba31 add Map() Has() and more 2012-08-30 13:43:44 -04:00
Martin Angers 07f9164340 start documenting, add Contains and Has 2012-08-30 12:16:33 -04:00
Martin Angers df16c7a894 split tests in multiple files 2012-08-29 21:35:09 -04:00
Martin Angers f94c18dc61 split in multiple small files 2012-08-29 21:14:23 -04:00