Commit Graph
509 Commits
Author SHA1 Message Date
Martin Angers bd4f51414c Merge branch 'master' into precise 2026-05-28 10:21:55 -04:00
Martin Angers 59eb2061a0 Merge pull request #561 from jvoisin/noclosure
perf: speed up Selection.Text by outlining recursion
2026-05-28 10:19:23 -04:00
Martin Angers 7f540a45a4 Merge pull request #560 from jvoisin/if
perf: pre-check raw class attribute in HasClass
2026-05-28 10:17: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
Martin Angers b2b7542bc5 Merge pull request #557 from jvoisin/winnow-nodes-direct
perf: drop grep/closure indirection in winnowNodes
2026-05-28 10:09:16 -04:00
Martin Angers c38303d8bf Merge pull request #550 from jvoisin/nomap
perf: skip dedup map in mapNodes for single-node selections
2026-05-28 10:05:55 -04:00
Martin Angers 32e5d34ae4 Merge pull request #547 from jvoisin/idx
perf: replace Index() PrevAll traversal with direct sibling walk
2026-05-28 10:03:04 -04:00
Martin Angers 69b9b29a1f Merge pull request #559 from jvoisin/is-matcher-early-return
perf: short-circuit IsMatcher on the first match
2026-05-28 09:52:50 -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 25b585d336 perf: skip dedup map in mapNodes for single-node selections
mapNodes always allocated a map[*html.Node]bool for deduplication, even
when called with a single source node where duplicates cannot arise.
Short-circuit to return the callback result directly when len(nodes)==1.
Also add a special case for when there are zero nodes.

This benefits all traversal methods (Find, Children, Parent, Next, Prev,
etc.) when operating on a single-node selection, which is the common
case after First(), Eq(), or directly on a Document.

name      old ns/op   new ns/op   delta
Find-8    23200       14200       -38.8%

name      old B/op    new B/op    delta
Find-8    4632        1496        -67.7%

name      old allocs  new allocs  delta
Find-8    27          13          -51.9%
2026-05-27 21:17:38 +02:00
jvoisin ece69fc2fd perf: speed up Selection.Text by outlining recursion
Replace the recursive closure in Selection.Text with a helper method call.
This keeps behavior unchanged while reducing runtime overhead in the hot path.

Benchmark (BenchmarkText, benchstat, n=12):
- sec/op: 2.133us -> 1.692us (-20.70%, p=0.000)
- B/op: 504 -> 504 (no change)
- allocs/op: 6 -> 6 (no change)
2026-05-27 21:12:36 +02:00
jvoisin 3b71495757 perf: pre-check raw class attribute in HasClass
HasClass normalized every matching element's class attribute with
classTrimReplacer before checking for the target token, even when the
raw attribute value could not possibly contain that class at all.

Add a cheap strings.Contains(attr.Val, rawClass) pre-check and only run
the replacer on plausible matches. This preserves behavior for class
attributes containing tabs/newlines while skipping most of the work on
misses.

BenchmarkHasClass on arm64 over 10 runs:

name         old time/op   new time/op   delta
HasClass-8    9.809µs       2.660µs      -72.89% (p=0.000)
2026-05-27 15:26:22 +02:00
jvoisin 535ec7c4f2 perf: short-circuit IsMatcher on the first match
IsMatcher handled multi-node selections by calling m.Filter(s.Nodes) and
checking whether the returned slice was non-empty, forcing the matcher to walk
the whole selection and materialize all matches even though the API only needs
an existence check.

This commit replaces this with a simple loop over s.Nodes that calls m.Match(n)
and returns true on the first match. This also makes the single-node special
case unnecessary.

Benchmark results on arm64 over 10 runs:

name            old time/op   new time/op   delta
Is-8             4.67µs        1.32µs       -71.7%
IsPositional-8   21.44µs       0.82µs       -96.2%

name            old B/op      new B/op      delta
Is-8             96           80           -16.7%
IsPositional-8   1144         184          -83.9%

name            old allocs    new allocs    delta
Is-8             5            4            -20.0%
IsPositional-8   11           7            -36.4%
2026-05-27 15:19:58 +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
Martin Angers e2468fa06d Merge pull request #552 from jvoisin/spaceclass
perf: replace regex+Split with strings.Fields in getClassesSlice
2026-05-26 09:12:25 -04:00
Martin Angers 5350c00a95 Merge branch 'master' into spaceclass 2026-05-26 09:11:05 -04:00
Martin Angers af20c4b0a0 Merge pull request #553 from jvoisin/slyce
perf: call Match/isInSlice directly in Until traversal helpers
2026-05-26 09:00:57 -04:00
Martin Angers b27992034a Merge pull request #551 from jvoisin/nore
perf: replace rxClassTrim regexp with strings.Replacer
2026-05-26 08:59:19 -04:00
Martin Angers 67dec88260 Merge pull request #548 from jvoisin/shortcircuit
perf: short-circuit IsFunction, IsSelection, and IsNodes
2026-05-26 08:53:05 -04:00
Martin Angers cb1755d318 Merge pull request #554 from jvoisin/gpn
perf: drop per-parent Selection allocation in getParentsNodes
2026-05-26 08:49:01 -04:00
jvoisin 5d01e23f35 perf: drop per-parent Selection allocation in getParentsNodes
Mirrors #553 for getSiblingNodes, now extended to the ancestor traversal path.
2026-05-25 20:52:51 +02:00
jvoisin b7030c526f perf: call Match/isInSlice directly in Until traversal helpers
getParentsNodes and getSiblingNodes created a newSingleSelection per
ancestor/sibling node just to call IsMatcher or IsNodes for the
until-stop condition. Since these are always single-node checks, call
stopm.Match(p) and isInSlice(stopNodes, p) directly, avoiding the
Selection allocation and the method dispatch overhead.

name                      old ns/op   new ns/op   delta
ParentsUntil-8            30700       16100       -47.6%
ParentsUntilNodes-8       82700       28400       -65.7%
NextUntil-8               38000       25700       -32.4%
NextUntilNodes-8          13700       5900        -56.9%
PrevUntil-8               109600      82600       -24.6%
PrevUntilNodes-8          11600       4600        -60.3%

name                      old B/op    new B/op    delta
ParentsUntil-8            11528       4248        -63.2%
ParentsUntilNodes-8       39040       10448       -73.2%
NextUntil-8               8592        7712        -10.2%
NextUntilNodes-8          5536        1304        -76.4%
PrevUntil-8               30344       28280       -6.8%
PrevUntilNodes-8          4248        920         -78.3%

name                      old allocs  new allocs  delta
ParentsUntil-8            304         44          -85.5%
ParentsUntilNodes-8       904         60          -93.4%
NextUntil-8               216         106         -50.9%
NextUntilNodes-8          156         27          -82.7%
PrevUntil-8               408         150         -63.2%
PrevUntilNodes-8          129         23          -82.2%
2026-05-24 22:48:16 +02:00
jvoisin c19544b145 perf: replace regex+Split with strings.Fields in getClassesSlice
getClassesSlice used rxClassTrim.ReplaceAllString followed by
strings.Split, which allocated twice and produced empty strings from
leading/trailing spaces. strings.Fields handles all Unicode whitespace
in a single pass with no empty entries.

name             old ns/op   new ns/op   delta
AddClass-8       176700      107500      -39.2%
RemoveClass-8    142900      64600       -54.8%

name             old B/op    new B/op    delta
AddClass-8       12544       7024        -44.0%
RemoveClass-8    6503        1777        -72.7%

name             old allocs  new allocs  delta
AddClass-8       697         374         -46.3%
RemoveClass-8    697         374         -46.3%
2026-05-24 22:38:53 +02:00
jvoisin 41a5c22149 perf: replace rxClassTrim regexp with strings.Replacer
The regex [\t\r\n] only does single-byte substitution, which is exactly
what strings.Replacer's byteReplacer is optimized for. This removes the
regexp dependency from property.go and drops the redundant ContainsAny
fast-path guard in HasClass, since byteReplacer already returns the
original string unchanged (no allocation) when nothing matches.

Benchmark shows ~18% improvement on HasClass with 0 allocs either way.
2026-05-24 22:30:18 +02:00
Martin Angers 83a0a8bbd8 Merge pull request #545 from jvoisin/nog
perf: avoid per-node Selection allocation in winnow Not path
2026-05-24 10:19:43 -04:00
Martin Angers cb2cc9d25e Merge pull request #549 from jvoisin/lack_of_class
perf: skip regex in getClassesAndAttr when no \t\r\n present
2026-05-24 10:07:42 -04:00
jvoisin 3c647f82fe perf: skip regex in getClassesAndAttr when no \t\r\n present
getClassesAndAttr unconditionally ran rxClassTrim.ReplaceAllString to
normalize tabs, carriage returns, and newlines in class attribute values.
The vast majority of HTML class attributes contain none of these, so add
a strings.ContainsAny fast path that falls back to simple concatenation.

This benefits all callers: HasClass, AddClass, RemoveClass, ToggleClass. Here's
a benchmark of HasClass to illustrate the gains:

name          old ns/op   new ns/op   delta
HasClass-8    222000      45300       -79.6%

name          old B/op    new B/op    delta
HasClass-8    19152       4280        -77.6%

name          old allocs  new allocs  delta
HasClass-8    1300        325         -75.0%
2026-05-23 23:11:00 +02:00
jvoisin d94aeb90e8 perf: short-circuit IsFunction, IsSelection, and IsNodes
These methods were building a full filtered Selection (via
FilterFunction/FilterSelection/FilterNodes -> winnow -> grep) only to
check Length() > 0. This commit replaces with early-return loops that stop at
the first match, significantly speeding them up:

name             old ns/op   new ns/op   delta
IsFunction-8     1530        3.9         -99.7%
IsSelection-8    1890        19.9        -98.9%
IsNodes-8        2016        19.9        -99.0%

name             old B/op    new B/op    delta
IsFunction-8     784         0           -100.0%
IsSelection-8    72          0           -100.0%
IsNodes-8        72          0           -100.0%

name             old allocs  new allocs  delta
IsFunction-8     28          0           -100.0%
IsSelection-8    3           0           -100.0%
IsNodes-8        3           0           -100.0%
2026-05-23 22:57:17 +02:00
jvoisin 5f0afd53e4 perf: replace Index() PrevAll traversal with direct sibling walk
Index() was creating a Selection, calling PrevAll() which traversed
through getSiblingNodes -> mapNodes (allocating a dedup map and
intermediate slices), then calling Length() on the result.

This commit replaces this with a direct PrevSibling pointer walk that counts
element nodes, bringing the number of allocations down to zero instead of a
couple of hundreds on local benchmarks
2026-05-23 22:50:53 +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 33466f5bcd Merge pull request #544 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.55.0
Bump golang.org/x/net from 0.54.0 to 0.55.0
2026-05-22 08:52:45 -04:00
dependabot[bot] 33ce051cd9 Bump golang.org/x/net from 0.54.0 to 0.55.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.54.0 to 0.55.0.
- [Commits](https://github.com/golang/net/compare/v0.54.0...v0.55.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.55.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-22 05:52:54 +00:00
Martin Angers 8c2d01c469 Merge pull request #543 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.54.0
Bump golang.org/x/net from 0.53.0 to 0.54.0
2026-05-13 21:34:12 -04:00
dependabot[bot] 21b9b64897 Bump golang.org/x/net from 0.53.0 to 0.54.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.53.0 to 0.54.0.
- [Commits](https://github.com/golang/net/compare/v0.53.0...v0.54.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.54.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-11 06:41:06 +00:00
Martin Angers c92c45163d Merge pull request #542 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.53.0
Bump golang.org/x/net from 0.52.0 to 0.53.0
2026-04-11 09:47:15 -04:00
dependabot[bot] 43d03d8533 Bump golang.org/x/net from 0.52.0 to 0.53.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.52.0 to 0.53.0.
- [Commits](https://github.com/golang/net/compare/v0.52.0...v0.53.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.53.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-10 05:52:49 +00:00
Martin Angers 401642b253 Update readme to prepare for 1.12 release v1.12.0 2026-03-15 12:26:31 -04:00
Martin Angers afd9326761 Merge pull request #540 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.52.0 2026-03-15 12:25:04 -04:00
dependabot[bot] f799f78b6e Bump golang.org/x/net from 0.50.0 to 0.52.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.50.0 to 0.52.0.
- [Commits](https://github.com/golang/net/compare/v0.50.0...v0.52.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.52.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-15 16:21:07 +00:00
Martin Angers 805d7b1d5c Update CI to 1.25 and 1.26 2026-03-15 12:19:53 -04:00
Martin Angers 7dc7e6463b Merge pull request #538 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.50.0 2026-02-13 10:10:41 -05:00
dependabot[bot] 3020ebb46d Bump golang.org/x/net from 0.49.0 to 0.50.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.49.0 to 0.50.0.
- [Commits](https://github.com/golang/net/compare/v0.49.0...v0.50.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.50.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-10 05:52:35 +00:00
Martin Angers ee4bcdb889 Merge pull request #537 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.49.0
Bump golang.org/x/net from 0.48.0 to 0.49.0
2026-01-17 10:29:26 -05:00
dependabot[bot] f3bc303dcb Bump golang.org/x/net from 0.48.0 to 0.49.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.48.0 to 0.49.0.
- [Commits](https://github.com/golang/net/compare/v0.48.0...v0.49.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.49.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-13 07:22:25 +00:00
Martin Angers 0f22828bca Merge pull request #536 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.48.0
Bump golang.org/x/net from 0.47.0 to 0.48.0
2025-12-23 09:13:59 -05:00
dependabot[bot] 68de6a15bb Bump golang.org/x/net from 0.47.0 to 0.48.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.47.0 to 0.48.0.
- [Commits](https://github.com/golang/net/compare/v0.47.0...v0.48.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.48.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-09 05:05:12 +00:00
Martin Angers 8ef6c9c294 Merge pull request #535 from PuerkitoBio/dependabot/github_actions/actions/checkout-6
Bump actions/checkout from 5 to 6
2025-11-21 16:01:03 -05:00
dependabot[bot] ffba93fcef Bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-21 05:20:57 +00:00
Martin Angers d3e3ce5663 Merge pull request #534 from PuerkitoBio/mna-prepare-1.11
Update readme for 1.11 release
v1.11.0
2025-11-16 11:05:17 -05:00