Skip to content

fix(graph): a path search keeps one predecessor per node, so it stays linear - #3116

Merged
colbymchenry merged 1 commit into
mainfrom
claude/pr-1583-linear-path-search
Oct 30, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/pr-1583-linear-path-search

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

The shortest-path search stops copying the whole path into every queue entry, so a long path or a dense graph no longer costs quadratic time and heap. This is the path part of #1583, split out as its own commit.

Root cause

findPath kept a full copy of the path so far in each queue entry and dequeued with Array.shift(). A chain of N hops copied 1 + 2 + ... + N elements.

Fix (from the PR)

Each discovered node keeps one predecessor in a map. The queue holds ids with a head index, and the path is built once at the end. The enqueue-once set from #2016 stays. Targets are fetched once each even when parallel edges point at the same node.

What we changed

  • Added a regression test with no timing in it: a 400-hop chain counts the array elements walked (81,001 on main, under 4,000 now).
  • The CHANGELOG bullet.

The test from #2016 now counts node lookups per id instead of patching Array.prototype.push, because the queue no longer holds objects with a path. Its expectation is unchanged. That change is the contributor's.

Validation

  • Red on main: the 400-hop test, and the PR's own test of a fan-in batch that asks for one node twice.
  • Same answers: 400 seeded pairs each on excalidraw, gin and flask give the same node and edge sequence on both arms (1,200 of 1,200).
  • A 20,000-hop chain: 1,007 ms and 47 MB of heap on main, 30 ms and 13 MB here.
  • Agent-facing output: none changes. findPath backs the library's findPath only, so codegraph_explore and its budgets are not touched.
  • Full suite on 12b1ca2b (this commit before a clean rebase, Vitest 4, CODEGRAPH_KERNEL_EXPECT=1, 4 workers): 840 files, 9,883 passed, 0 failed, 87 skipped. The rebase brought in nothing that shares a file with this commit besides CHANGELOG.md; graph.test.ts passes 40 of 40 on this head.

Lands #1583 (path part) by @danusha2345.

🤖 Generated with Claude Code

… linear

findPath's breadth-first search copied the whole path so far into every
queue entry and dequeued with Array.shift(). A long path cost time and heap
quadratic in its length, and a queued frontier held one copy per entry.

The search now keeps one predecessor per discovered node in a map, walks a
queue of ids with a head index, and builds the path once at the end. It keeps
the enqueue-once set from PR #2016. Targets are fetched once each even when
parallel edges point at the same node.

Red, green and A/B:
- Red on main: a 400-hop chain walks 81,001 array elements, against under
  4,000 now, and a fan-in batch asks for the same node twice. No timing in
  either test.
- Same paths: 400 seeded pairs each on excalidraw, gin and flask gave the same
  node and edge sequence on both arms (1,200 of 1,200).
- A 20,000-hop chain: 1,007 ms and 47 MB of heap on main, 30 ms and 13 MB here.

The test added in PR #2016 now counts node lookups per id instead of patching
Array.prototype.push, because the queue no longer holds objects with a path.
Its expectation is unchanged.

Original PR #1583 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry merged commit d494e43 into main Oct 30, 2026
@colbymchenry
colbymchenry deleted the claude/pr-1583-linear-path-search branch October 30, 2026 23:23
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.

1 participant