Skip to content

Break long ear fans in the hashed loop - #212

Merged
mourner merged 2 commits into
mapbox:mainfrom
EpicDima:epicdima/break-long-ear-fans
Oct 6, 2026
Merged

mourner merged 2 commits into
mapbox:mainfrom
EpicDima:epicdima/break-long-ear-fans

Conversation

@EpicDima

@EpicDima EpicDima commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #211.

After 32 ears cut in a row, the hashed loop now steps one vertex further if that vertex is convex. That ends the fan described in #211. Shorter fans and polygons with up to 80 vertices (the non-hashed path) are not affected. 119,677 of the 119,680 polygons in the tiles fixture get the same triangles as before, and all test expectations still hold.

Median ms per call; each cell shows two separate runs as first ; second:

input 3.2.4 this PR
regular 10k-gon 76.4 ; 73.0 1.67 ; 1.66
regular 100k-gon 7014 ; 6997 24.2 ; 24.3
rounded rectangle, 10k 72.8 ; 72.9 1.34 ; 1.36
star with n/2 spikes, 10k 41.9 ; 41.7 17.8 ; 17.3
thin wavy corridor, 10k 43.0 ; 43.3 18.8 ; 18.8
bench/bench-tiles.js 360 ; 361 362 ; 360

The one slowdown I found: right after a regular 10k-gon in the same process, a 10k circle with a ±15 % random radius takes 9.21 ; 8.90 ms vs 6.83 ; 6.66 ms on 3.2.4. In a fresh process both take 7.3 ; 7.1 ms; see Details.

Details
  • Why 32. It is the smallest limit at which none of the 2,361 tile polygons above 80 vertices does more work than in 3.2.4. With 16, 6 of them do more work, up to +0.8 %. With 8, 131 do, up to +11 %. A regular 10k-gon does 1.1 % of the 3.2.4 work.
  • Why only past a convex vertex. Stepping past a reflex vertex sends the loop around the ring to the next ear. Without this check, a 100k corridor does 16 % more work.
  • Why not restore ear = next.next. It fixes circles too, but a comb with n/4 deep teeth gets slower than in 3.2.4: 10k takes 114 ; 106 → 181 ; 182 ms.
  • Correctness. Every triangle still passes the same ear test, and the skipped vertex is visited again before the loop decides there are no ears left. So the limit only changes the order of cuts. I checked the result exactly, in BigInt coordinates: on the tiles fixture, the test fixtures and the shapes above, the triangles cover the polygon without gaps or overlaps exactly where 3.2.4's do.
  • The slowdown above. 3.2.4 gets the same slowdown on the random-radius circle when a star runs first in the process: 8.82 ; 8.64 ms (this PR: 9.32 ; 8.87 ms). After the tiles fixture, both versions are back to normal speed. This looks like a JIT effect in isEarHashed rather than more work, so I left it out of scope.
  • Small polygons. Timed in alternation on the 117,319 tile polygons with up to 80 vertices, this PR takes 1.0006 ; 1.0070 of the 3.2.4 time. An identical copy of 3.2.4 takes 0.9961 ; 0.9986, so any difference is within noise.
  • What this PR does not fix. A star with n/2 spikes is quadratic in every version. At 100k it takes 3174 ; 3088 ms here vs 2162 ; 1974 ms in 3.0.2 (the 3.0.2 numbers are from another run, at load average 3–5). 3.2.4 with ear = next.next restored does the same work on it as this PR, so that gap does not come from the fan.

Apple M3 Pro, Node v26.8.2, load average 1.4–3.4.

Since 3.1.0 (mapbox#195) the loop continues from the vertex after a cut ear, so on a
convex run every ear is cut from the same vertex. The fan triangles are long,
their z-order range covers a large part of the polygon, and each hashed ear
check scans O(n) points: a regular 10k-gon is triangulated in quadratic time.

After 32 ears cut in a row, step one vertex further if that vertex is convex
(past a reflex one the loop would walk the ring to the next ear). Shorter fans
are left alone: 119,677 of the 119,680 polygons in the tiles fixture get the
same triangles as before.
@EpicDima
EpicDima requested a review from a team as a code owner October 5, 2026 14:08
@EpicDima
EpicDima requested review from mourner and removed request for a team October 5, 2026 14:08
No vertex of a strictly convex ring is reflex, so no point can block an ear and every point check
passes; cutting an ear keeps the ring convex. The loop now skips the point checks for such a ring
until the first candidate fails, instead of stepping past the apex of a long fan. The output is
the same as in 3.2.4 index for index.

@mourner mourner left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, looks good now!

@mourner
mourner merged commit 2c222c7 into mapbox:main Oct 6, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Quadratic time on convex rings since 3.1.0 (10k-vertex circle: 79 ms vs 1.9 ms in 3.0.2)

2 participants