wave9b: 455/204 — graphhopper-0001/0002 + valhalla-0001 + OSRM CLEAN

This commit is contained in:
russell@unturf.com 2026-03-27 16:54:46 -04:00
parent 16cc7ecf1d
commit 81bc62b9cb
9 changed files with 811 additions and 5 deletions

View file

@ -0,0 +1,79 @@
# valhalla-0001: linkclassification.cc — IsSlipLane() nested linear scan O(F×R)
## File
`src/mjolnir/linkclassification.cc`
## Severity
**MEDIUM**
## Lines Affected
- Lines 658662: `std::find(forward_nodes.begin(), forward_nodes.end(), node)` inside
`for (auto node : reverse_nodes)` loop
## Defective Code
```cpp
// IsSlipLane() — lines 648673
bool IsSlipLane(Data& data, SlipLaneInput input, double traverse_threshold) {
auto forward_nodes =
GoTowardsIntersection(input.first_node, input.fork_edge, true, traverse_threshold, data);
auto reverse_nodes =
GoTowardsIntersection(input.last_node, input.merge_edge, false, traverse_threshold, data);
// O(R × F) — for each of R reverse nodes, linear scan over F forward nodes
std::optional<uint32_t> intersection_node;
for (auto node : reverse_nodes) {
if (std::find(forward_nodes.begin(), forward_nodes.end(), node) != forward_nodes.end()) {
intersection_node = node;
break;
}
}
return intersection_node != std::nullopt;
}
```
`forward_nodes` and `reverse_nodes` are `std::vector<uint32_t>`. For each node in
`reverse_nodes` (up to R nodes), `std::find` does a linear scan over `forward_nodes`
(up to F nodes). Total work: O(R × F).
Note: `GoTowardsIntersection` already uses an `std::unordered_set<size_t>` for its own
visited tracking, so visited tracking is O(1) there — but the intersection check reverts
to O(N²).
`IsSlipLane` is called during graph tile building (`mjolnir`) for every candidate link edge
in the OSM road network. Dense urban areas may have thousands of slip lane candidates, and
the traverse threshold controls path lengths. At high thresholds both vectors can reach
hundreds of nodes.
## Complexity Analysis
| Path | Per intersection check | Call frequency | Overall |
|------|----------------------|----------------|---------|
| Slow (std::find on vector) | O(F × R) | per link edge | **O(L × F × R)** |
| Fast (unordered_set lookup) | O(R) | per link edge | **O(L × R)** |
Where L = number of link edges, F = forward path length, R = reverse path length.
At traverse_threshold = 200m and urban density, F and R can each reach ~50 nodes,
giving 2500× overhead per link check vs O(R).
## Fixed Code
```cpp
bool IsSlipLane(Data& data, SlipLaneInput input, double traverse_threshold) {
auto forward_nodes =
GoTowardsIntersection(input.first_node, input.fork_edge, true, traverse_threshold, data);
auto reverse_nodes =
GoTowardsIntersection(input.last_node, input.merge_edge, false, traverse_threshold, data);
// Build O(1) lookup set from forward_nodes
std::unordered_set<uint32_t> forward_set(forward_nodes.begin(), forward_nodes.end());
std::optional<uint32_t> intersection_node;
for (auto node : reverse_nodes) {
if (forward_set.count(node)) { // O(1) instead of O(F)
intersection_node = node;
break;
}
}
return intersection_node != std::nullopt;
}
```

View file

@ -0,0 +1,228 @@
package unit;
import java.util.*;
/**
* Unit test for valhalla-0001:
* linkclassification.cc IsSlipLane() nested linear scan O(F×R).
*
* Simulates the intersection-check loop:
* for (node : reverse_nodes) // R iterations
* std::find(forward_nodes.begin(), ..., node) // O(F) linear scan
*
* vs. the fixed version using unordered_set:
* forward_set = unordered_set(forward_nodes)
* for (node : reverse_nodes) // R iterations
* forward_set.count(node) // O(1) hash lookup
*
* Compile: javac -d . IsSlipLaneAlgorithm.java
* Run: java -ea unit.IsSlipLaneAlgorithm
*/
public class IsSlipLaneAlgorithm {
static int checkCount = 0;
static int passCount = 0;
static void check(String desc, boolean cond) {
checkCount++;
if (!cond) {
System.out.println("FAIL [" + checkCount + "]: " + desc);
} else {
passCount++;
System.out.println("PASS [" + checkCount + "]: " + desc);
}
}
// -------------------------------------------------------------------------
// Slow path: std::find linear scan over forward_nodes for each reverse node
// -------------------------------------------------------------------------
static class SlowIntersectionFinder {
int ops = 0;
boolean find(int[] forwardNodes, int target) {
for (int n : forwardNodes) {
ops++;
if (n == target) return true;
}
return false;
}
/** Returns intersection node index, or -1 if none */
int findIntersection(int[] forwardNodes, int[] reverseNodes) {
for (int node : reverseNodes) {
if (find(forwardNodes, node)) {
return node;
}
}
return -1;
}
}
// -------------------------------------------------------------------------
// Fast path: unordered_set (HashSet) O(1) lookup
// -------------------------------------------------------------------------
static class FastIntersectionFinder {
int ops = 0;
int findIntersection(int[] forwardNodes, int[] reverseNodes) {
// Build set from forward_nodes once O(F)
Set<Integer> forwardSet = new HashSet<>(forwardNodes.length * 2);
for (int n : forwardNodes) forwardSet.add(n);
for (int node : reverseNodes) {
ops++;
if (forwardSet.contains(node)) {
return node;
}
}
return -1;
}
}
// -------------------------------------------------------------------------
// Build test data: two vectors of node IDs that share a suffix
// forward_nodes: 0, 1, 2, ..., F-1
// reverse_nodes: F*2, F*2+1, ..., F*2+R-sharedSuffix-1, then F-sharedSuffix, ..., F-1
// Intersection is at node F-sharedSuffix (last shared node in forward, first in overlap)
// If sharedSuffix == 0, no intersection exists (worst-case: scan all R × F)
// -------------------------------------------------------------------------
static int[] buildForwardNodes(int F) {
int[] nodes = new int[F];
for (int i = 0; i < F; i++) nodes[i] = i;
return nodes;
}
static int[] buildReverseNodes(int F, int R, int sharedSuffix) {
// First (R - sharedSuffix) nodes are unique to reverse, rest overlap with forward tail
int[] nodes = new int[R];
int uniqueCount = R - sharedSuffix;
for (int i = 0; i < uniqueCount; i++) {
nodes[i] = F * 2 + i; // disjoint from forward
}
for (int i = 0; i < sharedSuffix; i++) {
nodes[uniqueCount + i] = F - sharedSuffix + i; // overlaps with forward tail
}
return nodes;
}
public static void main(String[] args) {
System.out.println("=== valhalla-0001: IsSlipLane forward_nodes linear scan ===\n");
// --- Test 1: correctness both paths find the same intersection ---
{
int F = 20, R = 20, shared = 5;
int[] fwd = buildForwardNodes(F);
int[] rev = buildReverseNodes(F, R, shared);
SlowIntersectionFinder slow = new SlowIntersectionFinder();
FastIntersectionFinder fast = new FastIntersectionFinder();
int slowResult = slow.findIntersection(fwd, rev);
int fastResult = fast.findIntersection(fwd, rev);
check("correctness: intersection found by both paths",
slowResult != -1 && fastResult != -1);
check("correctness: both agree on intersection node ("
+ slowResult + " == " + fastResult + ")", slowResult == fastResult);
}
// --- Test 2: correctness no intersection case ---
{
int F = 20, R = 20, shared = 0;
int[] fwd = buildForwardNodes(F);
int[] rev = buildReverseNodes(F, R, shared);
SlowIntersectionFinder slow = new SlowIntersectionFinder();
FastIntersectionFinder fast = new FastIntersectionFinder();
int slowResult = slow.findIntersection(fwd, rev);
int fastResult = fast.findIntersection(fwd, rev);
check("no-intersection: both return -1 (slow=" + slowResult + " fast=" + fastResult + ")",
slowResult == -1 && fastResult == -1);
}
// --- Test 3: complexity worst case (no intersection) ---
{
// No shared nodes slow does F×R comparisons
int F = 200, R = 200;
int[] fwd = buildForwardNodes(F);
int[] rev = buildReverseNodes(F, R, 0); // no intersection
SlowIntersectionFinder slow = new SlowIntersectionFinder();
FastIntersectionFinder fast = new FastIntersectionFinder();
slow.findIntersection(fwd, rev);
fast.findIntersection(fwd, rev);
long expectedSlowOps = (long) F * R; // scans all F for each of R reverse nodes
double ratio = (double) slow.ops / fast.ops;
check("worst-case: slow_ops == F*R ("
+ slow.ops + " == " + expectedSlowOps + ")", slow.ops == expectedSlowOps);
check("worst-case: fast_ops == R ("
+ fast.ops + " == " + R + ")", fast.ops == R);
check("worst-case: ratio >= 10x (actual " + String.format("%.1f", ratio) + "x)",
ratio >= 10.0);
System.out.println(" slow_ops=" + slow.ops + " fast_ops=" + fast.ops
+ " ratio=" + String.format("%.0f", ratio) + "x (F=" + F + " R=" + R + ")");
}
// --- Test 4: complexity early exit (intersection at start of reverse) ---
{
// Intersection at first reverse node slow does F comparisons, fast does 1
int F = 200, R = 200;
int[] fwd = buildForwardNodes(F);
// Make first reverse node = 0 (first forward node found immediately in fast,
// but slow scans forward until it finds it = position 0 = 1 op)
int[] rev = new int[R];
rev[0] = 0; // immediately in forward set
for (int i = 1; i < R; i++) rev[i] = F * 2 + i; // rest disjoint
SlowIntersectionFinder slow = new SlowIntersectionFinder();
FastIntersectionFinder fast = new FastIntersectionFinder();
int slowResult = slow.findIntersection(fwd, rev);
int fastResult = fast.findIntersection(fwd, rev);
check("early-exit: both find node 0", slowResult == 0 && fastResult == 0);
check("early-exit: slow_ops=1 (found at start of forward)",
slow.ops == 1);
check("early-exit: fast_ops=1 (hash lookup)", fast.ops == 1);
}
// --- Test 5: complexity intersection at END of forward (worst linear scan) ---
{
// Intersection at forward[F-1] slow must scan all F nodes before finding match
int F = 300, R = 100;
int[] fwd = buildForwardNodes(F);
// rev[0] = F-1 found, but only after scanning all F forward nodes (slow)
int[] rev = new int[R];
rev[0] = F - 1;
for (int i = 1; i < R; i++) rev[i] = F * 2 + i;
SlowIntersectionFinder slow = new SlowIntersectionFinder();
FastIntersectionFinder fast = new FastIntersectionFinder();
slow.findIntersection(fwd, rev);
fast.findIntersection(fwd, rev);
double ratio = (double) slow.ops / fast.ops;
check("tail-match: slow_ops == F (scanned all forward, slow=" + slow.ops + ")",
slow.ops == F);
check("tail-match: fast_ops == 1 (hash, fast=" + fast.ops + ")", fast.ops == 1);
check("tail-match: ratio >= F (" + String.format("%.0f", ratio) + "x >= " + F + "x)",
ratio >= F);
System.out.println(" slow_ops=" + slow.ops + " fast_ops=" + fast.ops
+ " ratio=" + String.format("%.0f", ratio) + "x (F=" + F + " R=" + R + ")");
}
System.out.println("\n" + passCount + "/" + checkCount + " PASS");
}
}