Skip to content

Allocation-free VertexLinker edge distance on packed geometry#7688

Merged
vpaturet merged 6 commits into
opentripplanner:dev-2.xfrom
entur:vertexlinker-compact-distance
Jun 18, 2026
Merged

Allocation-free VertexLinker edge distance on packed geometry#7688
vpaturet merged 6 commits into
opentripplanner:dev-2.xfrom
entur:vertexlinker-compact-distance

Conversation

@vpaturet

@vpaturet vpaturet commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Motivation

VertexLinker.distance() materializes a full JTS LineString per candidate edge, projects it (cloning every Coordinate), and runs JTS DistanceOp — producing CoordinateXY, PackedCoordinateSequence, Envelope, GeometryLocation for every candidate edge on the linking hot path (and the GBFS rental-linking at startup).

This PR provides an allocation-free alternative implementation of the same algorithm.

What changed

  1. Allocation-free edge distance. New EndpointContextLineString.squaredDistanceToPointEquirectangular(...) walks the packed delta geometry directly via the existing DlugoszVarLenIntPacker.Decoder, computing the same per-segment point-to-segment distance JTS DistanceOp produces (mathematically equivalent up to floating-point rounding) — with zero allocation and no LineString materialization. StreetEdge exposes it; VertexLinker.distance() becomes a one-liner. findSplitVertex() (once per winning edge, not per candidate) is intentionally left on JTS.

  2. Squared distance. The linker only orders edges and applies thresholds (both monotonic), so the per-candidate sqrt is dropped — the method returns the squared distance and the search radius is squared once. The one consumer needing real distance (the per-mode duplicate-way dedup, an additive epsilon) takes a single sqrt per mode group: dSq <= (closest + eps)², exactly equivalent to d <= closest + eps. This moves sqrt from O(candidate edges) to O(modes).

  3. Two cohesive packed-geometry types. The packed byte[] format backs two contracts, now expressed as two single-responsibility types that share one private delta engine:

    • CompactLineString — the self-contained value object (endpoints baked into the packed bytes; used by CompactLineStringSequence).
    • EndpointContextLineString (new, stateless) — the endpoint-context contract, where the endpoints are supplied externally by StreetEdge and omitted from the bytes (compact / uncompact / squaredDistanceToPointEquirectangular).

    The shared encode/decode engine (packIntermediateDeltas / decodeInto + the fixed-point conversions) stays package-private on CompactLineString and is reused, so there is no logic duplication. StreetEdge keeps its raw byte[] field and delegates to EndpointContextLineStringno per-edge wrapper object, no extra indirection, no serialization change. (This replaces the old CompactLineStringUtils.)

Performance (local A/B: Norway graph, 68 GBFS rental networks, 9,995 real production trip queries; JFR + async-profiler)

Allocation sample counts on the linking path (JFR; startup/GBFS-apply window → warm-load window), confirmed independently by async-profiler:

Site Baseline This PR
VertexLinker.distance 10,797 / 5,198 0 / 0
JTS DistanceOp 5,692 / 2,710 0 / 0
equirectangularProject 2,872 / 1,451 12 / 2
CoordinateXY 3,006 / 1,348 582 / 58 (−81% / −96%)

The new walk appears in CPU samples but never in allocation samples (the Decoder scalar-replaces). Warm-load distance CPU dropped 1,619 → 54 samples.

@vpaturet vpaturet changed the title Vertexlinker compact distance Allocation-free VertexLinker edge distance on packed geometry (no JTS DistanceOp) Jun 3, 2026
@vpaturet vpaturet added !Optimization The feature is to improve performance. Entur Test This is currently being tested at Entur labels Jun 3, 2026
@codecov

codecov Bot commented Jun 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.00000% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.06%. Comparing base (d25d924) to head (1a7fe4a).
⚠️ Report is 27 commits behind head on dev-2.x.

Files with missing lines Patch % Lines
...ner/street/geometry/EndpointContextLineString.java 92.40% 1 Missing and 5 partials ⚠️
...g/opentripplanner/street/linking/VertexLinker.java 91.66% 1 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             dev-2.x    #7688      +/-   ##
=============================================
+ Coverage      74.04%   74.06%   +0.02%     
- Complexity     21830    21879      +49     
=============================================
  Files           2423     2426       +3     
  Lines          85772    85930     +158     
  Branches        8536     8563      +27     
=============================================
+ Hits           63507    63644     +137     
- Misses         19235    19249      +14     
- Partials        3030     3037       +7     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@vpaturet vpaturet changed the title Allocation-free VertexLinker edge distance on packed geometry (no JTS DistanceOp) Allocation-free VertexLinker edge distance on packed geometry Jun 3, 2026
vpaturet added 2 commits June 3, 2026 15:09
…eometry

VertexLinker.distance() materialized a full JTS LineString from the packed
edge geometry, cloned every coordinate into an equirectangular projection,
and ran JTS DistanceOp - allocating Coordinate[], CoordinateXY,
PackedCoordinateSequence, Envelope and GeometryLocation for every candidate
edge on the linking hot path (and the GBFS-apply path at startup).

Add CompactLineStringUtils.distanceToPointEquirectangular(), which streams
the segments straight from the Dlugosz-packed deltas via the existing
Decoder and runs the same per-segment point-to-segment math JTS uses, with
zero allocation and no LineString materialization. StreetEdge exposes it via
distanceToPointEquirectangular(); VertexLinker.distance() becomes a one-liner.

findSplitVertex() (once per winning edge) is intentionally left on JTS.

CompactLineStringDistanceTest checks parity against the old JTS path
(equirectangularProject(uncompactLineString(...)).distance) to <1e-12 across
straight, single/multi-intermediate, reversed, degenerate and on-vertex cases
plus 1000 random polylines.
…andidate sqrt

The vertex linker only orders edges and applies thresholds, both monotonic in
distance, so the per-candidate sqrt in the edge-distance computation is
unnecessary. Return the squared distance instead
(squaredDistanceToPointEquirectangular) and square the search-radius threshold
once per call.

The one consumer that needs real distance - the per-mode duplicate-way dedup,
which uses an additive epsilon (closest + DUPLICATE_WAY_EPSILON_DEGREES) that
squaring does not preserve - takes a single sqrt per mode group and compares
against the squared band: dSq <= (closest + eps)^2 is exactly d <= closest + eps.
This moves sqrt from O(candidate edges) to O(modes) with identical edge
selection (linking integration tests unchanged).
@vpaturet vpaturet force-pushed the vertexlinker-compact-distance branch from 86dbf03 to be97d09 Compare June 3, 2026 13:12
@vpaturet vpaturet added Entur Test This is currently being tested at Entur and removed Entur Test This is currently being tested at Entur labels Jun 3, 2026
@vpaturet vpaturet force-pushed the vertexlinker-compact-distance branch from be97d09 to aa170a8 Compare June 5, 2026 12:40
…LineString

Reorganize the packed line-string geometry API into two cohesive types instead
of CompactLineString + CompactLineStringUtils:

- CompactLineString: the self-contained value-object contract (endpoints baked
  into the packed bytes; used by CompactLineStringSequence).
- EndpointContextLineString (new, stateless): the endpoint-context contract,
  where endpoints are supplied externally by StreetEdge and omitted from the
  packed bytes -- compact() / uncompact() / squaredDistanceToPointEquirectangular().

The shared delta engine (packIntermediateDeltas / decodeInto / the fixed-point
conversions) stays package-private in CompactLineString and is reused by both,
so there is no logic duplication. StreetEdge keeps its raw byte[] geometry field
and delegates to EndpointContextLineString -- no per-edge wrapper object and no
serialization change.

CompactLineStringUtils is removed.

Tests: CompactLineStringTest (value object); EndpointContextLineStringTest and
EndpointContextLineStringDistanceTest (endpoint-context round-trip + JTS parity).
@vpaturet vpaturet force-pushed the vertexlinker-compact-distance branch from aa170a8 to 0fa5445 Compare June 5, 2026 13:08
@vpaturet vpaturet marked this pull request as ready for review June 5, 2026 13:16
@vpaturet vpaturet requested a review from a team as a code owner June 5, 2026 13:16
@vpaturet vpaturet added the +Skip Changelog This is not a relevant change for a product owner since last release. label Jun 5, 2026
Comment thread street/src/main/java/org/opentripplanner/street/model/edge/StreetEdge.java Outdated

@leonardehrenfried leonardehrenfried 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.

Claude and myself could not find anything wrong with this but I agree with the small issues that @optionsome has raised.

vpaturet added 2 commits June 16, 2026 10:20
…distance

# Conflicts:
#	street/src/main/java/org/opentripplanner/street/model/edge/StreetEdge.java
- Rename squaredDistanceToPointEquirectangular -> squaredEquirectangularDistanceToPoint
- Remove the seeded-Random fuzz test, keep the deterministic parity cases
- Inline the single-use walk helper, rename jtsOracle -> jtsReferenceDistance
@vpaturet vpaturet added this pull request to the merge queue Jun 18, 2026
Merged via the queue into opentripplanner:dev-2.x with commit 1fee988 Jun 18, 2026
8 checks passed
@vpaturet vpaturet deleted the vertexlinker-compact-distance branch June 18, 2026 08:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Entur Test This is currently being tested at Entur !Optimization The feature is to improve performance. +Skip Changelog This is not a relevant change for a product owner since last release.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants