Skip to content

tui: first draw takes 14.6s on a cold service start #59597

Open

Description

@Addin

Description

On a cold launch in a large repo, the TUI's first frame takes 14.6 s. The devtools bar (debug.devtools: true, debug.timing: true) reads Time to first draw: 14589.25ms. Opening the TUI against an already-running service on the same machine connects in 122 ms.

Most of the wait is the per-launch managed service, not rendering:

  • +0 to +10.20 s: the TUI spawns opencode serve --service; the child logs cli starting, spends 3.89 s with no further log line, then 5.30 s in location services booted (4.21 s of that after watcher setup).
  • +10.56 s to +12.56 s: the first event-stream attempt times out after 2.0 s.
  • +14.59 s: first frame.

The frame lands before the event-stream retry (+15.45 s), so the 2.9 s backoff is not the direct cost, but the frame is still 2.0 s after the timeout.

Plugins

None configured: no plugin key in opencode.jsonc and no local plugin directories. The service logs plugins=14 during reconciliation, so those are built-ins.

OpenCode version

2.2.0, channel latest, local=false, standalone install at ~/.opencode/bin/opencode.

Steps to reproduce

  1. Set debug.devtools: true and debug.timing: true.
  2. Open the TUI in a large repo, then close it. The managed service receives SIGTERM (shutdown started signal=SIGTERM).
  3. Open the TUI in the same directory again.
  4. Read Time to first draw in the devtools bar.

Cold starts only. A warm attach to an already-running service connects in 0.12 s.

Screenshot

The devtools overlay from this launch reads:

Time to first draw: 14589.25ms
FPS: 55
Frame: 23.648ms (avg: 19.398ms)
Render: 0.034ms (avg: 0.044ms)
Memory: 50.83MB
Threaded: No

Operating System

Pop!_OS 24.04, kernel 7.2.6-76070206-generic, x86_64.

Terminal

Ghostty (TERM_PROGRAM=ghostty, TERM=xterm-256color, COLORTERM=truecolor), shell /bin/bash.

Additional context

Timeline for the 2026-10-27 11:10 launch (run ids shortened, project path redacted):

wall clock from TUI start event
11:10:12.197 0 cli starting (TUI, args=[])
11:10:13.163 +0.97 s background service starting reason=missing
11:10:13.258 +1.06 s child cli starting (serve --service)
11:10:17.149 +4.95 s first server log line (git rev-parse); 3.89 s after the child started, nothing logged in between
11:10:18.15 +5.95 s watchers start for /etc, /, /home, ~, ~/development, ~/development/<repo>
11:10:22.395 +10.20 s location services booted durationMs=5295
11:10:22.760 +10.56 s event-stream connect attempt 0
11:10:24.761 +12.56 s Timed out connecting to server (2.0 s timeout)
11:10:26.786 +14.59 s first frame, derived from the 14,589.25 ms metric
11:10:27.650 +15.45 s event-stream retry
11:10:27.804 +15.61 s event stream connected

Log excerpt:

11:10:12.197 cli starting args=[] version=2.2.0
11:10:13.163 background service starting reason=missing
11:10:13.258 cli starting args=["serve","--service"]
11:10:17.149 spawning process command=git ...   (3.89 s silent gap)
11:10:18.154 watcher subscribe path=/etc type=entries ignores=0
11:10:18.155 watcher subscribe path=/ type=entries ignores=0
11:10:18.155 watcher subscribe path=/home type=entries ignores=0
11:10:18.156 watcher subscribe path=~ type=entries ignores=0
11:10:18.156 watcher subscribe path=~/development type=entries ignores=0
11:10:18.157 watcher subscribe path=~/development/<repo> type=entries ignores=0
11:10:22.395 location services booted durationMs=5295   (4.21 s silent gap)
11:10:22.760 event stream connecting attempt=0
11:10:24.761 event stream disconnected error="Timed out connecting to server"
11:10:27.650 event stream connecting attempt=1
11:10:27.804 event stream connected

The open questions are what runs during the two silent gaps and what gates the frame between location services booted and the first render. Instrumenting both gaps would make the boot budget visible; drawing the shell before the service is ready would remove the 14.6 s empty terminal.

Activity

  1. opencode-agent commented on Oct 27, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed timeline. I tried to reproduce this on 2.2.0 (Linux x86_64, no plugins), with the managed service stopped before each launch and a git repo of about 150k files at ~/development/repo. I couldn't get the slow boot:

    • Warm page cache: location services booted durationMs=55, and the event stream connected on the first attempt about 1.7 s after launch.
    • After dropping the page cache: location services booted durationMs=97. The event stream again connected on the first attempt, with no Timed out connecting to server.

    I didn't see either silent gap in the service. With a cold page cache, the one slow step I saw was in the TUI process itself: about 5 s between cli starting and background service starting.

    Your log has the slow boots on only 3 of 282 launches, so this seems to depend on something on your machine. These would help:

    1. A --log-level debug service log from a slow cold launch, especially the lines inside the two gaps (cli starting → first git spawn, and watcher subscribe → location services booted).
    2. Roughly how many files are in the repo and under ~, and whether the disk is an SSD or a spinning HDD, local or on a network or encrypted volume.
    3. Whether it still happens if you run sync; echo 3 | sudo tee /proc/sys/vm/drop_caches before launching.

    Related issues, most likely first:

  2. Addin commented on Oct 27, 2026

    @Addin
    Author

    @opencode-agent
    Consolidating the earlier data and the follow-up tests into one answer.

    Storage and files

    • Root, home, the repo, the 236 MB opencode binary and the SQLite store are on /dev/sdb: a Sabrent 0bda:9210 RTL9210 M.2 NVMe bridge, crypto_LUKS -> LVM -> ext4 (220 GB, 75% used).
    • The hub is a VIA Labs USB 3.0 hub (2109:0817, 5000M on Bus 002), but the SSD is attached through its USB 2.0 companion (2109:2817, Bus 001) and enumerates at 480 Mbps with bcdUSB 2.10. The hub is 3.0; the SSD link is 2.0 (cable or adapter). At 480 Mbps a 236 MB cold read is roughly 4-6 s, the size of the first gap.
    • Repo: 151,183 files / 1.9 GB (62,460 under node_modules). Home: 1,571,054 files; find ~ -xdev -type f took 67.5 s (3.9 s user, 9.3 s sys, the rest I/O).

    The slow launch was the first after a reboot

    uptime -s reports the boot at 12:08:15 local (11:08:15Z). The log shows the previous TUI's service getting SIGTERM at 11:07:53Z and the next TUI starting at 11:10:12Z: the 14,589.25 ms draw ran with a cold page cache, 1m57s after boot, right after the 2.2.0 upgrade. The other two slow boots (Oct 20 15:50, Oct 21 08:27) are also first launches after long idle. Most warm launches are 30-250 ms. A plain reboot on 13:38:12 gave 3478.41 ms (below), so the 14.6 s is the post-upgrade case, not the steady cold-boot cost.

    drop_caches + service restart: 373 ms, no repro

    /proc/vmstat shows drop_pagecache 1, so the drop ran. A new service came up at 12:03:51.982Z and reached location services booted at 12:03:52.593Z (373 ms); the TUI reconnected in 0.6 s. No 3.89 s or 4.21 s gap. This is not a true cold boot: the running service kept the 236 MB binary mapped (mapped pages are not evicted), and dirty SQLite pages are not evicted either. A real cold boot needs the service to stop before the drop, or a reboot.

    First-frame gate (TUI-side probe, 2.2.0, warm service)

    • Dead server (--server http://127.0.0.1:1): the CLI exits with Could not reach server ... ECONNREFUSED. There is no shell-first path at startup; the "Connection lost" overlay only covers a disconnect after the TUI is up.
    • Warm managed service: first draw is 1494.76 ms (re-run 1522.99 ms). Sequence: update check 12:23:49.384Z -> event stream connecting 50.140 -> connected 50.162 -> plugin reconciliation id=1 50.199-50.225 (26 ms, 14 plugins) -> first draw ~50.3 -> reconciliation id=2 50.319.
    • Cold launch mapping: the TUI waited 10.56 s before its first SSE attempt (the wait for the managed service to become reachable), the first attempt timed out after 2.0 s, and the first draw came 2.0 s later at +14.59 s, before the retry at +15.45 s. So the draw waits for the first attempt to settle, not for a successful reconnect. Plugin setup (26 ms) and the SSE connect (22 ms) are not the slow part.

    Post-reboot re-test

    • Reboot 13:38:12 local; first TUI 13:40:15 (2m03s after boot). TTFD 3478.41 ms.
    • Service: cli starting 12:40:15.935Z -> first git 17.346 (+1.41 s) -> location services booted 17.977 (664 ms). TUI event stream connected 19.420 (+4.03 s); no timeout or retry.
    • The bar then re-mounted and showed 5478.41 ms with no new TUI process (same run 9fd09e2d). The metric latches per bar instance, not per process, so it grows after the first draw; the 14.6 s screenshot may also be a later mount.
    • The 480 Mbps link still adds ~2 s over the warm floor (~1.5 s), but the 10.2 s service boot did not recur.

    Debug log

    After the reboot the fresh TUI spawn did apply the env: the serve --service pid has OPENCODE_LOG_LEVEL=debug in /proc/<pid>/environ and logs level=DEBUG (Skipped models.dev APIs..., skill source loaded). The first DEBUG line is at 19.076, after location services booted, so the 1.41 s and 0.66 s gaps are still empty at debug level. They are not log-instrumented; strace -T -f on the service start is the remaining way to see them.

    The earlier service restart path (TUI still running) did not apply the env: the respawned child had no OPENCODE_LOG_LEVEL and no DEBUG lines, and --log-level debug never reaches the child. That is #59610.

    strace

    Two traces, both warm:

    • An isolated serve: execve -> database schema bootstrap 0.42 s; every traced syscall under 1 ms.
    • The managed serve --service with the TUI connecting: execve 13:50:26.767 -> DB open 27.160 (0.39 s); the location boot 58.235 -> 58.351 (125 ms), 279 syscalls, 53 getdents64. The largest getdents64 returned 147 entries (the top-level home), so this is not a recursive 1.57 M-file walk. The slowest syscall was 2.1 ms.

    There is no blocking syscall and no syscall-volume explosion. The cold 3.89 s and 4.21 s are consistent with the same small set of opens, getdents64, git children and config lookups at cold-disk latency (480 Mbps + LUKS) plus page faults, which strace does not trace. A cold trace needs the service stopped and the cache dropped (or a reboot) before the traced start.

    Repro

    Reboot, then start the TUI once. The plain-reboot re-test gave 3478.41 ms (service boot 664 ms), not 14.6 s; the 14.6 s case followed the 2.2.0 upgrade. The bar then re-mounts and shows 5478.41 ms, so the metric overstates the first draw. The software fix is to draw the shell before the service is ready.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions