Performance & Logs
How Daintree manages memory, CPU and disk on its own: resource profiles, memory pressure, freeing memory, disk-space monitoring, Linux file watching, error boundaries, developer mode, and logs.
Daintree is built to run five to ten agents at once, each with its own terminal, dev server, and Chromium renderer. That is a lot of pressure on one laptop, so a fair amount of the app is devoted to adapting to it. This page is the map of that machinery: what adapts automatically, what you can actually change, and where to look when something feels slow.
Automatic Resource Management
Resource profiles
Daintree runs in one of three internal resource profiles: performance, balanced, or efficiency. The profile decides how often the app polls git, how many worktrees keep live file watchers, how quickly terminals fall back from WebGL to DOM rendering, and how long the anti-flash timeouts are when you switch projects.
A background service samples the machine every 30 seconds and sums a pressure score:
| Signal | Contribution |
|---|---|
| Daintree's own memory | +1 past a low fraction of physical RAM, +2 past a high one |
| Running on battery | +1 |
| Thermal pressure (macOS) | +1 serious, +2 critical |
| OS CPU speed limit (macOS, Windows) | +1 below 100%, +2 below 50% |
| Active agent fleet size | +1 at 8 agents, +2 at 16, +3 at 24 |
| System memory still available | +1 when low, +3 when critical |
| Terminal-workload memory | +1 past a quarter of RAM, +2 past 40%, capped so workloads alone can't force efficiency |
A score of 0 means performance; 3 or more means efficiency; anything between is balanced. The thresholds scale with how much RAM the machine has, so a 64 GB desktop isn't judged by a 16 GB laptop's standards.
Transitions are asymmetric on purpose. Dropping toward efficiency needs the new target to hold for 30 seconds; climbing back toward performance needs 90. Protecting the machine is urgent; a brief lull shouldn't undo throttling that's still needed. Separately, if the app's main thread is genuinely saturated (high tail latency and high loop occupancy, which rules out garbage-collection blips and native UI work), it drops straight to efficiency with no hold at all.
Profile changes are silent. There's no toast and no badge. If you want to know which profile you're in and why, open the Why slow? tab in the Diagnostics Dock: it shows the current profile, the target profile, the pressure score, and named badges for each active pressure.
The clearest way to feel a profile shift is polling cadence. Under efficiency, git status refreshes slow down, background worktrees poll instead of watching, and terminals move to the DOM renderer sooner. Stopping a few agents or plugging in the charger lowers the score and, after the hold window, moves you back up.
The file-watcher budget
One profile lever is worth calling out because it explains a symptom people notice. Daintree caps how many background worktrees keep a git file watcher: 20 under performance, 12 under balanced, 6 under efficiency. The worktree you're focused on always keeps its full recursive watcher and is excluded from the cap. The most recently focused background worktrees keep lighter .git-only watchers, and everything past the cap falls back to polling.
So in a session with twenty worktrees open, a status change in one you haven't touched for an hour may take a poll interval to appear. That's the budget working, not a bug. Revocations run before grants, so the number of live OS file handles stays bounded even mid-reconcile, which is what keeps a long session from slowly exhausting the inotify limit on Linux.
Memory pressure
When the app's memory footprint climbs, mitigation runs in two tiers.
Tier 1 fires once at the start of a pressure episode and at most once every five minutes while it persists. It trims pty-host state and destroys hidden browser and dev-preview webviews. It deliberately does not clear caches or force garbage collection in the visible renderer, because that would make the window you're looking at janky in order to save memory you weren't short of.
Tier 2 needs three consecutive pressure samples, rechecks the system-memory signal after tier 1 ran, and is limited to once every ten minutes. It destroys hidden webviews and evicts cached project renderers down to the one you're using. A cached project renderer is a full Chromium renderer (a few hundred megabytes each), so this is by far the largest thing Daintree can reclaim. A single clean sample resets the episode, so a later independent spike can react immediately instead of sitting out a cooldown.
Separately, inside the pty-host, a fast governor watches heap use every 2 seconds and pauses terminal output when memory approaches its budget. Terminals in that state show a flow-status pill on their header and resume on their own once the pressure clears. There's no toast, because it recovers by itself and escalating would only train you to ignore it.
Freeing memory yourself
Two explicit actions reclaim memory when you'd rather not wait for the automatic paths.
Free memory is on a project's row in the project switcher. It's offered for any project that isn't the active one, isn't missing, and hasn't already been closed: the active project owns the live renderer, and the others have nothing to reclaim. A confirm dialog names the project, lists any running processes and active or waiting agents that will be stopped, and states plainly that sessions are preserved and restored when you reopen the project. That's the important part: this is not "lose your work", it's "put the project down and pick it back up".
Auto-close idle projects lives in Settings → General. It is off by default. When on, background projects that have no terminals at all and have been idle past the threshold are closed automatically. The presets are 15 minutes, 30 minutes, 1 hour, and 2 hours; the accepted range is 15 minutes to 24 hours. The active project is never touched, and reopening a project restores its panels. Because the condition is "no terminals", this never closes something with an agent in it.
Daintree can also nudge you: when a background project's terminals have been idle past a threshold, an inbox notification names the project and offers Close them (or to mute that project). That action does tear down the terminals, because you asked for it: scrollback is flushed first, so the sessions come back when you reopen the project.
The agent compile cache
Node-based agent CLIs write a V8 compile cache under Daintree's user-data directory, one subdirectory per agent and Node version. It makes agent startup measurably faster, and left unbounded it grows without limit: one report measured 9.3 GB across 890,589 files.
It's now bounded on three axes: entries expire after 30 days, and the whole cache is capped at 1 GiB and 100,000 files across every agent combined. The sweep runs in two passes, dropping expired version directories first and then trimming the newest-first survivors until they fit. Deletions are best-effort and isolated: a file locked by a live agent on Windows is counted and left for the next sweep.
The sweep is silent by design. It logs a summary and never notifies you, because a cache reclaiming its own disk is not an event you need to be told about.
Disk space
Daintree polls the volume holding its application data every 5 minutes. Note that this is not necessarily your system drive.
| State | Threshold | What happens |
|---|---|---|
| Warning | below 2000 MB | Notification: "Low disk space warning: {N} MB remaining on the application data volume. Free disk space to avoid data loss." |
| Critical | below 500 MB | Notification: "Critical: Disk space very low. Only {N} MB remaining. Session backups and terminal snapshots have been paused." Writes are suppressed. |
Recovery uses a 50 MB hysteresis band at each edge, so a volume hovering right on a threshold doesn't flap between states, and a recovery from critical steps through warning rather than jumping straight to normal. An escalation into critical bypasses the notification cooldown: that one is worth interrupting you for.
At critical, the suppression flag is published before the next log call, so the log line that would have reported it is itself dropped. That's intentional: under a full disk, the last thing that should consume the remaining bytes is Daintree explaining that there are no bytes left.
Free space on that volume and normal operation resumes on the next poll. Session backups and terminal snapshots restart on their own.
Linux: File Watching Degraded
On Linux, Daintree uses the kernel's inotify subsystem to watch worktree files. Each watched directory takes one inotify watch, and the kernel caps the total per user through the fs.inotify.max_user_watches sysctl. Large projects, deep node_modules trees, or several worktrees at once can exhaust the limit, especially with editors, browsers, and language servers watching too.
When the watcher hits the limit, the kernel returns ENOSPC. It's the same code as "no space left on device", but here it means the watch pool, not your disk. Daintree surfaces a warning toast titled File watching degraded with the message "Linux inotify watch limit reached. Some files may not auto-refresh until you raise it." and a Copy fix command action.
A related failure is EMFILE, raised when the per-user inotify instance limit is exhausted rather than the watch limit: several processes each holding open inotify descriptors. The fix command below raises both limits at once.
Only one toast fires per app session, however many worktrees hit the limit. While degraded, the toolbar Problems button carries a small amber pip so the state stays visible after the toast has gone.
Retry budget and wake recovery
The retry budget can be reset up to three times per session. Two triggers reset it: a user-driven workspace refresh, and wake from sleep. Wake recovery matters on Linux specifically, because suspending often leaves inotify watches in a half-dead state: without the reset you'd see degraded watching after every resume.
The reset isn't gated by exhaustion. Any non-zero retry counter is cleared, so a manual refresh during a backoff window pulls the watcher back to immediate mode without waiting out the timer. When the watcher recovers, the degraded indicator clears itself.
Applying the fix
The temporary command applies immediately and resets on reboot:
sudo sysctl -w fs.inotify.max_user_watches=524288 fs.inotify.max_user_instances=512 To persist it, write both values into a drop-in file and reload:
sudo tee /etc/sysctl.d/99-inotify.conf <<'EOF'
fs.inotify.max_user_watches=524288
fs.inotify.max_user_instances=512
EOF
sudo sysctl --system The default watch limit on most distributions is 8192, though kernels 5.11 and later may auto-scale it with available RAM. 524288 is the value VS Code, JetBrains, and most of the Electron ecosystem recommend. Each watch costs roughly 1 KB of kernel memory, and only materializes if you actually create that many watches. The instance limit defaults to 128; raising it to 512 covers running several Electron apps side by side.
macOS hits the same wall differently
macOS uses FSEvents rather than inotify, so there's no per-directory descriptor to exhaust. It can still hit the system-wide file-descriptor ceiling, and when it does Daintree takes the same degraded-polling path with the same warning pip. The macOS fix is the descriptor limit rather than a sysctl:
Temporary, for the current boot:
sudo launchctl limit maxfiles 65536 524288 To persist it, create a launch daemon at /Library/LaunchDaemons/limit.maxfiles.plist. Editing /etc/sysctl.conf is not reliably respected on macOS 14 and later.
Windows uses directory-change notifications with no comparable per-user cap, so this failure mode doesn't occur.
See the inotify commands above.
When Parts of the UI Fail
Every major UI surface is wrapped in an error boundary, so a render-time exception in one piece doesn't take down the app. If a settings panel, a dialog, or a worktree card throws while rendering, the boundary catches it and you get a localized fallback in that surface only.
Where the boundaries sit
At the section level (sidebar, main content, help panel, diagnostics dock), and again at finer granularity around each docked panel, grid panel, and worktree card. Every lazily loaded palette, dialog, and modal host also gets its own named boundary: the quick switcher, the action palette, the project switcher, the settings dialog, the plugin manager, the shortcut reference, the terminal info host, and each confirm dialog. Naming them individually means a crash report identifies the surface that failed rather than "somewhere in the modal layer".
When a boundary catches an exception, three things happen:
- The error is reported through the renderer-exception handler.
- It's added to the in-app error store with a correlation ID.
- A local fallback renders inside the broken surface, with a copyable Error ID.
The Error ID also appears in the Problems tab of the Diagnostics Dock, so a trace can be lined up against the exact row that fired.
Recovery is local: reopen the dialog, hide and reshow the panel, or reload the renderer from the Help menu. The rest of the app keeps running while you do.
Recurring errors
Daintree counts how often the same error fingerprint occurs. At five occurrences, the escalation strategy changes.
That number is a count, not a rate. It has no time window, and it persists across sessions: five occurrences spread over a week reach the threshold exactly as five in a minute do. The reasoning is that an error which keeps coming back after restarts is a different problem from one that fired five times in a burst, and retrying it another time is not going to help.
Two surfaces respond differently:
- Inline error banner: the secondary action flips from Retry to View errors, which opens the Problems tab where you can read the history and copy the Error ID.
- Toasts: Retry is suppressed rather than relabeled, and the action becomes Copy details, which copies a structured snapshot. There's no View errors at toast level.
The same escalation fires early when an error has already been promoted into the dock, or when retry was explicitly exhausted: the surface changes as soon as further retries stop being useful, without waiting for the count.
Cross-session counting is held in a persisted fingerprint store, so the Problems tab can show that an error has appeared across multiple launches. On a conflict the store prefers the incoming counter, so a stale renderer cache can't hide an escalation.
Developer Mode and Profiling
Developer mode is in Settings → Troubleshooting. It's a parent switch ("Activates all debugging features below") with two children that only become available once it's on:
- Auto-open diagnostics dock: opens the dock at startup.
- Focus events tab: makes the dock land on Events when it auto-opens. Only available when auto-open is on.
There's no secret gesture and no hidden flag; it's an ordinary setting in an ordinary tab.
DevTools
Open Chromium DevTools with Cmd+Alt+I on macOS or Ctrl+Shift+I on Windows and Linux. They're available in development builds by default; in production builds, turn on developer mode first.
Record a CPU profile
The Troubleshooting tab has a Record CPU profile section that captures a 15-second profile of the app's interface. Click Record profile, reproduce the slow interaction (the button shows a live countdown) and it stops itself, or click Stop recording early. The saved .cpuprofile file opens directly in Chrome DevTools.
Attaching DevTools to the window mid-recording ends it, since the two profilers can't share the renderer. Daintree says so rather than silently producing nothing.
System health check
The same tab has a Run health check button that verifies Git, Node.js, and npm are present and reports each version. It's the fastest way to confirm that a "command not found" inside an agent terminal is a PATH problem rather than a Daintree problem.
Building from source
If you're running from source, npm run perf drives the performance harness, and DAINTREE_PERF_CAPTURE=1 (or npm run dev:perf) turns on internal performance capture. Results feed the CI budgets table in the Diagnostics Dock's Perf tab. See Build from Source.
Accessing Logs
Daintree keeps a rotating log file of app activity, errors, and diagnostic detail. The Logs tab in the Diagnostics Dock streams the same content live; the file on disk is for offline reading and for attaching to bug reports.
Where the file is
The log is daintree.log in a logs/ directory inside Daintree's user-data directory:
~/Library/Application Support/Daintree/logs/daintree.log
%APPDATA%\Daintree\logs\daintree.log
~/.config/Daintree/logs/daintree.log
It rotates at 5 MiB, keeping 5 files: daintree.log plus daintree.log.1 through daintree.log.5. All six go into a diagnostics bundle.
Reading them
- In the app: the Logs tab, with severity filters, a source multi-select, search, and per-entry copy.
- On disk: Settings → Troubleshooting → Open Log File opens the current file in your system editor.
- Clear Logs in the same section wipes the buffer behind a confirmation, so a misclick doesn't discard history you wanted for a report.
The last 100 lines of the previous session are surfaced at the top of the Logs tab in their own card, so you can line up a current symptom against whatever happened just before the last shutdown without opening a second file.
Verbose logging
- This session: toggle Verbose logging in Settings → Troubleshooting. It resets on app restart, deliberately: verbose logging costs performance and log size, and leaving it on by accident is a worse outcome than having to turn it on twice.
- Persistently: launch with
DAINTREE_DEBUG=1set. That's the supported route for anything that has to survive a restart, including a crash you're trying to reproduce.
Per-module log levels
Below the verbose toggle is a Per-module log levels section. Set Log Level… opens a palette where you pick a module (or a process-wide wildcard) and a level. Active overrides are listed underneath, with Clear All Overrides to drop them.
Unlike the verbose toggle, these overrides persist across restarts. That asymmetry is the point: verbose-everything is a blunt instrument you want to forget about, while "debug-level logging for the git watcher only" is something you turn on deliberately and want to still be there tomorrow.