Skip to content

[HN-102] Knowledge graph build is O(N²) and freezes the tab on every autosave #465

Description

@ixjosemi

Problem

collectRawPathReferences builds and runs one RegExp per candidate path, per document:

function hasRawPathReference(content: string, path: string): boolean {
  const pattern = new RegExp(`(^|[^A-Za-z0-9_./-])${escapeRegExp(path)}(?=$|[^A-Za-z0-9_./-])`)
  return pattern.test(content)
}

function collectRawPathReferences(content: string, availablePaths: string[]): string[] {
  const normalizedContent = content.replace(/\\/g, '/')
  return availablePaths.filter((path) => hasRawPathReference(normalizedContent, path))
}

It is called from for (const sourcePath of filePaths), so the whole graph build is N² regex compilations and N² full-content scans over the vault.

This runs synchronously inside a useMemo on the main thread, and it re-runs on every autosave while the graph panel is open. The invalidation chain:

knowledge-graph-panel.tsx:338  graph = useMemo(..., [agentSources, contentByPath, markdownPaths, openFileContentByPath])
knowledge-graph-panel.tsx:333  openFileContentByPath = useMemo(..., [openFiles])
workspace-shell.tsx:1365       openFiles = useMemo(..., [openFilePaths, fileCache])
workspace-shell.tsx:1051       handleSaveFile → setFileCache(...)
use-editor-drafts.ts:29        debounceMs = 600

Location

apps/web/src/lib/kb-graph.ts:75-95 (hasRawPathReference, collectRawPathReferences), called from :139-155

Impact

Measured by copying the three functions verbatim into an isolated benchmark:

vault size regex compiles actual (ms) with precompiled regex (ms) speedup
50 2 500 16.5 5.9 2.8x
100 10 000 76.3 36.0 2.1x
250 62 500 450.3 256.5 1.8x
500 250 000 1 935.8 1 184.5 1.6x
1 000 1 000 000 9 491.6 5 164.6 1.8x

At 500 notes that is a ~2 s synchronous freeze of the whole tab, every time the editor autosaves. At 1 000 notes, ~9.5 s.

Suggested fix

Precompiling the regexes out of the loop is not enough — measured at only 1.6-1.8x, because the dominant cost is the N² content scans, not the compilations.

The fix has to be algorithmic: scan each document once, extracting path-shaped tokens, and resolve those against a Set of known paths. That turns O(N² x length) into O(N x length).

Better still, and it also solves the companion issue about the panel downloading the whole vault: build the graph server-side and return it from an endpoint, so neither the O(N²) nor the vault content ever touches the browser main thread.

Source: independent verification pass (Claude Opus 5), measured locally. Not part of the HN-001..HN-063 batch.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions