Skip to content

core: project-copy refresh can report a 48-second no-op #37793

Closed as not planned

Description

@kitlangton

Summary

Immediately after a 53.4-second cold Location acquisition, the forked ProjectCopy.refresh timer ran for another 48.1 seconds and completed with updated=[] and removed=[]. The log currently makes this look like 48 seconds of worktree discovery, but there is not enough stage-level evidence to distinguish actual refresh work from scheduler starvation or an unrelated synchronous post-boot task blocking the event loop.

This should be tracked separately from Location acquisition because the refresh starts only after the Location graph is available and runs in a scoped background fiber.

Environment

  • opencode version: 0.0.0-next-15834
  • OS: Darwin 25.5.0, arm64
  • Terminal: WarpTerminal, TERM=xterm-256color, COLORTERM=truecolor
  • Shell: /bin/zsh
  • Install/channel: global Bun install, next
  • Managed service: one existing version-matched service; same PID before and after the incident
  • Repository shape: large VCS-backed repository with 81 worktree entries at follow-up
  • Active plugins: three local user plugins; names and paths omitted

Reproduction

  1. Open a cold VCS-backed Location on the managed service.
  2. Wait for location services booted.
  3. Observe the automatically forked project copy refresh started and project copy refresh done records.
  4. Compare the refresh result, persisted project-directory count, direct Git command timings, service responsiveness, and event-loop delay.

The 48-second result was observed once in the same severe cold-start incident. Ordinary refreshes for this project usually complete in milliseconds to hundreds of milliseconds.

Expected Behavior

  • A no-change automatic refresh should finish quickly and should not delay health checks, SSE, plugin readiness, or other clients.
  • The timer should identify time spent loading persisted directories, checking paths, discovering each strategy, canonicalizing results, and committing changes.
  • Time waiting to be scheduled should be distinguishable from time spent executing refresh work.
  • Automatic discovery should be bounded for repositories with many linked worktrees.
  • If the project has no persisted source directories, the refresh should take the empty fast path without invoking Git.

Actual Behavior

Sanitized UTC timeline:

18:01:52.738  location services booted, durationMs=53359
18:01:52.791  project copy refresh started
18:02:40.711  repository watcher fallback is logged
18:02:40.841  project copy refresh done, updated=[], removed=[]

The measured refresh interval is 48,050 ms. The server emitted almost no durable application traffic during this period, but normal service recovery and MCP/plugin startup were delayed until the same boundary.

At the pinned revision, refreshAfterBoot starts the timer, forks copies.refresh, and returns from layer acquisition.

ProjectCopy.refresh has these stages:

  1. Read persisted project directories.
  2. Check every stored path with unbounded concurrency.
  3. Run every registered strategy for every existing source directory with unbounded concurrency.
  4. Deduplicate discovered directories.
  5. Apply updates/removals in one database transaction.
  6. Publish an update only if rows changed.

The built-in strategy performs repository discovery, git worktree list --porcelain, and filesystem canonicalization for every returned worktree at copy-strategies.ts:23-32. The Git subprocess boundary is git.ts:853-875, and parsing is git.ts:910-921.

Additional Context

Follow-up measurements

  • A direct repository-discovery command completed in 0.02 seconds.
  • git worktree list --porcelain returned 81 entries in 0.12 seconds.
  • Read-only follow-up inspection of the same channel database found zero current project_directory rows globally.

The current row count does not prove the exact table state at the incident timestamp, and the fast Git commands do not reproduce the original load. They do show that a persistent 48-second Git cost is not currently present. Because the timer measures wall time inside a forked fiber, the strongest honest conclusion is that the refresh fiber remained incomplete for 48 seconds, not that git worktree list itself consumed 48 seconds.

Instrumentation needed

Record elapsed execution time and scheduler delay for each refresh stage. Strategy discovery should identify the strategy ID, source ordinal, returned entry count, and duration, but never log directory values. Subprocess diagnostics should include command category, exit status, and duration without stdout.

Also record event-loop delay over the interval. If a different synchronous post-boot operation starves an otherwise empty refresh, the ProjectCopy timer should make that explicit rather than attributing all wall time to refresh.

Acceptance criteria

  • A no-directory refresh has a tested empty fast path and does not invoke any strategy.
  • A no-change refresh over a repository with at least 100 synthetic worktree entries completes within a documented non-gating benchmark budget.
  • Stage diagnostics distinguish database read, path checks, each strategy call, canonicalization, transaction, event publication, and scheduler delay.
  • One slow or unavailable source has a bounded timeout or yields an isolated error instead of delaying every source indefinitely.
  • Concurrent strategy/source work has a measured concurrency policy rather than unbounded fan-out by default.
  • The automatic post-boot refresh never delays Location readiness and does not make the shared health or event endpoints unresponsive.
  • A regression can reproduce either actual slow refresh work or scheduler starvation and names the correct owner.

Related

All paths, project IDs, worktree names, and Session data have been removed from this report.

Activity

  1. added
    bugSomething isn't working
    perfIndicates a performance issue or need for optimization
    coreAnything pertaining to core functionality of the application (opencode server stuff)
    on Jul 19, 2026
  2. self-assigned this
    on Jul 19, 2026
  3. neriousy commented on Oct 21, 2026

    @neriousy
    Member

    Outdated: #42149 replaced ProjectCopy with worktrees and removed the automatic post-boot refreshAfterBoot. Worktree.refresh now runs only when requested, so this 48s post-boot no-op no longer applies as written.

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

Metadata

Metadata

Assignees

Labels

2.0bugSomething isn't workingcoreAnything pertaining to core functionality of the application (opencode server stuff)perfIndicates a performance issue or need for optimization

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions