Skip to content

fix(core): start fff search lazily on first find - #58959

Merged
kitlangton merged 3 commits into
devfrom
kit/lazy-fff
Oct 25, 2026
Merged

kitlangton merged 3 commits into
devfrom
kit/lazy-fff

Conversation

@kitlangton

@kitlangton kitlangton commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

What it does. Every git Location used to start its own fff file finder as soon as it booted, even if nobody ever searched in it. Each finder brings its own scanner, watcher and git threads, so a server with a few dozen open projects carried about 150 extra threads from the start. Now the finder starts on the first file search in that Location.

How it works.

  1. fffLayer no longer calls Fff.create while the layer is built. The first find() creates the picker. The layer registers its finalizer before the picker exists, and creating and publishing the picker is one synchronous, uninterruptible step, so an interrupted search can't leave a native picker without an owner. A search after shutdown returns [] and creates nothing.
  2. A brand-new picker is still scanning, so that first find() waits for the scan to finish, up to 3 seconds, before searching. Before this change the boot-time head start hid that wait. After a timeout it returns whatever has been indexed so far, the same as searching during a scan used to.
  3. If creating the picker fails, the warning is logged once and search returns [], as before.

Picker gains isScanning(). Both the bun and node fff packages already have it. makeFffLayer(create) takes the backend so tests can drive scanning by hand; fffLayer is makeFffLayer(Fff.create).

Risk. Server only, file search (@ mentions, file pickers). The first search in each project now pays for the initial scan (milliseconds for a normal repo, capped at 3 s for a huge one). After that, search behaves as before: fff still runs its own watcher once started. Finders aren't evicted when idle yet, so projects you've searched in keep their threads until the Location shuts down.

Proof.

A bench script (not committed) builds fffLayer for N temporary git repos (500 files each) in one process. It samples ps -M thread count and RSS after the layers are built ("boot"), then again after one search per repo:

N Locations threads at boot, before → after RSS at boot, before → after threads after first search in all
10 83 → 31 58 → 49 MB 82
35 183 → 31 69 → 50 MB 182

The process baseline with no Locations is 30–31 threads and about 49 MB, so after this change boot adds no fff threads. Every search found its file in both runs.

  • bun test test/filesystem/search.test.ts: 9 pass. Three tests use a fake backend with TestClock: no picker until the first search, which stays pending until the scan finishes; partial results at the 3 s timeout, and later searches don't wait; an interrupted first search leaves the picker owned and destroyed exactly once when the layer closes. One live test with real fff checks that the very first search finds an existing file. With the scan wait removed, 3 of these fail.
  • bun run typecheck (packages/core): no type errors.
  • oxlint on changed files: clean.

An independent review of this PR asked for two fixes: tie picker ownership to the layer so an interrupted search can't leak it, and replace the timing-dependent scan test with TestClock tests. Both are done.


Added by the opencode bot (AI-generated).

Related: #54631, #37794

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The change looks sound. I checked it with a throwaway bench that built fffLayer for 10 git repos in one process. On the base, threads went from 31 to 79 as soon as the layers were built. With this PR they stay at 31 until the first search in each repo, and every first search found its file. search.test.ts passes (6 tests), and typecheck:tsc for packages/core is clean. The picker is created in one synchronous step, so concurrent first searches can't create two pickers. A creation failure is still logged once and returns [], and the picker is still destroyed with the layer scope.

Public API: unchanged. isScanning() is added only to the internal Picker interface.

One small suggestion on the new test (inline comment).

Related: #54631, #37794


expect(yield* paths("existing-alpha")).toContain(RelativePath.make("src/existing-alpha.ts"))

yield* Effect.promise(() => Bun.write(path.join(directory, "src/created-beta.ts"), "beta"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The first-search assertion above is the one that covers this change. The create and delete steps below mostly test fff's own watcher, using real filesystem events, polling and 10-second timeouts. That makes the test slower and more likely to be flaky on loaded CI machines, without checking anything new about lazy startup. Could you drop them and keep just the first search?

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The new commit looks sound. A search that is cancelled midway can no longer leave a picker that nothing cleans up: the layer registers its cleanup before the picker exists and creates it in one step that can't be interrupted. The failure warning is still logged only once, and a search after shutdown returns [] without creating anything. The three tests that use a fake backend run on TestClock, so they don't depend on real timing.

I re-ran my bench with this commit: 31 threads after building 10 layers, 79 after one search each, and all 10 searches found their file. search.test.ts passed 9 of 9 in three runs, and typecheck:tsc is clean.

Public API: unchanged. makeFffLayer and isScanning() are internal to packages/core.

My earlier comment about the live create/delete test is still open.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

My earlier comment is addressed. The live fff test now only checks that the first search finds an existing file, and the create/delete steps with 10-second timeouts are gone. This commit changes only the test file. search.test.ts passes (9 tests) and typecheck:tsc is clean. Public API: unchanged.

@kitlangton
kitlangton merged commit 9cc9e58 into dev Oct 25, 2026
14 checks passed
@kitlangton
kitlangton deleted the kit/lazy-fff branch October 25, 2026 15:08
@amrkmn amrkmn mentioned this pull request Oct 27, 2026
1 task
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant