Skip to content
Productivity
Command

/investigate-performance

Investigate a Nimbalyst performance problem (freeze, lag, idle CPU, slow op, memory growth) with measured evidence before proposing a fix.

From plugin
nimbalyst
1.4k35 skills3 agents35 commands
Install
$ npx -y skills add nimbalyst/nimbalyst --agent claude-code

How it fires

How this command gets triggered: by you, by Claude, or both.

  • Fires itselfClaude auto-loads it when your prompt matches the work.
  • You can call itInvoke it directly when you want it.
  • Slash command/investigate-performance

Context preview

What this command does when you run it.

Investigate a Nimbalyst performance problem (freeze, lag, idle CPU, slow op, memory growth) with measured evidence before proposing a fix.

Command definition

investigate-performance.md
description: Investigate a Nimbalyst performance problem (freeze, lag, idle CPU, slow op, memory growth) with measured evidence before proposing a fix.

Investigate Performance

You are a performance investigator for Nimbalyst. The user has a performance complaint — a freeze, a hitch, typing lag, a slow operation, high idle CPU, or memory growth. Your job is to **measure it, name the specific offender with numbers, and propose a ranked fix** — then ask how to proceed.

Do NOT jump to a fix. Do NOT theorize from reading code alone. Every performance claim in this repo's history that was made from "the code path looks wrong" has been wrong at least as often as it was right. The deliverable is numbers.

User's Complaint

$ARGUMENTS

---

Prime directives

1. **Run your own observation commands.** Never ask the user to paste logs, run `ps`, open Chrome DevTools, or click through the Developer Dashboard. You have log tools, `database_query`, `renderer_eval`, `Bash`, and heap-snapshot tools. See `docs/DEBUGGING_LOGS.md`. 2. **Identify the hot process before theorizing.** Main, renderer, GPU, a *non-focused* renderer, the SQLite worker, and a helper process all fail differently. Sample first. 3. **Separate the victim from the blocker.** Both database backends are single-lane. A query reported at 30s is usually 1s of work behind 29s of queue-wait. Fixing the victim fixes nothing. 4. **Baseline, then delta.** Capture a number before the fix and the same number after. A before/after table is the deliverable. "Looks memoized now" is not evidence. 5. **One change at a time.** Re-measure between changes or you will not know which one worked. 6. **Never run `npm run dev`. Never restart Nimbalyst without explicit permission.**

---

Step 1 — Classify the symptom

Pick the instrument from the symptom. Ask the user only if genuinely ambiguous.

| Symptom | First instruments | | --- | --- | | App freezes / "window became unresponsive" / beachball | `[PERF] Event loop lag` in main.log, auto-captured `.cpuprofile`, SQLite worker hot shapes | | One operation is slow (open session, open project, switch project, save) | `[IpcSlow]`, `database:getPerformance` byShape, `EXPLAIN QUERY PLAN` | | Slow/janky startup, "takes forever to become usable" | `[StartupSlow]`, `[StartupMaintenanceGate]`, main.log timeline from launch | | Typing lag, scroll jank, navigation repaints the whole window | renderer probes + a render-count vitest probe | | High CPU / fan / battery drain while idle | `ps` per-PID sampling, rAF/animation probes, hidden-window audit | | Memory grows over hours, eventual crash | `capture_heap_snapshot` / `analyze_heap_snapshot`, `dev:get-system-stats` deltas | | Sync/collab slowness or cost | `wrangler tail`, per-message round-trip counting, `docs/CLOUDFLARE_USAGE_COST_LESSONS.md` |

Check `mcp__nimbalyst-extension-dev__get_environment_info` first: dev vs packaged, and **which database backend is live** (PGLite or better-sqlite3). Several instruments below are backend-specific.

---

Step 2 — Gather evidence

2a. Main-process log greps (always do these)

The app self-instruments. Use `get_main_process_logs` with `searchTerm`, and `Bash` + `grep` against `~/Library/Application Support/@nimbalyst/electron/logs/main.log` when you need context lines.

| Grep | What it means | Source | | --- | --- | --- | | `[PERF] Event loop lag` | Main-thread block. Sampled every 250ms, logged at >=500ms. **A single line reports the whole blocked interval** — the previous lag line's timestamp brackets when the freeze began. | `main/utils/performanceMonitor.ts` | | `[PERF] High CPU usage` | 10s sampler, >50% main-process CPU, with heap/handles/requests counts | same | | `[PERF] Captured CPU profile (trigger=…) -> <path>` | A `.cpuprofile` was auto-written. Triggers: sustained >80% CPU, or a single lag >=2000ms. | same | | `[PERF] SQLite worker hot shapes (elu=…)` | **Top-10 query shapes by total time, dumped from inside the worker.** This usually answers "what is hot?" without opening any profile. Read this before anything else on a DB-shaped problem. | `database/sqlite/worker/sqliteWorker.ts` | | `[IpcSlow] <channel> took Nms` | Any `safeHandle` invocation over the threshold | `main/utils/ipcRegistry.ts` | | `[StartupSlow] <name> took Nms` | Startup phase over threshold | `main/utils/startupTiming.ts` | | `[StartupMaintenanceGate]` | When deferred maintenance was released and how long each task took | `main/services/startupMaintenanceGate.ts` | | `[SQLite Backup] Online backup complete {sizeBytes: …}` | **Database size.** A multi-GB DB amplifies every query and the backup itself competes for the worker. | | | `Queue full, dropping oldest event` | A bounded work queue is overflowing (file-edit attribution has done this under multi-session load) | | | `MaxListenersExceededWarning` | Listener leak — see `docs/IPC_LISTENERS.md` | | | `took \d\d\d\dms` | Catch-all for four-digit durations anyone logged | |

2b. CPU profiles — read them yourself

Profiles land in `~/Library/Application Support/@nimbalyst/electron/logs/`:

  • `cpu-<iso>.cpuprofile` — main process
  • `cpu-sqlite-worker-<iso>.cpuprofile` — the SQLite worker isolate (the main-side profile shows this as idle, so a saturated worker is invisible without it)

That directory holds thousands of files. **Sort by mtime and take only the ones bracketing the incident**: `ls -t ~/Library/Application\ Support/@nimbalyst/electron/logs/*.cpuprofile | head -5`.

A `.cpuprofile` is JSON (`nodes`, `samples`, `timeDeltas`). Do not hand it to the user for Chrome DevTools — aggregate it yourself with a throwaway script in `temptests/`:

// temptests/readprofile.mjs — self-time by function, top 25
import { readFileSync } from 'node:fs';
const p = JSON.parse(readFileSync(process.argv[2], 'utf8'));
const byId = new Map(p.nodes.map(n => [n.id, n]));
const self = new Map();
for (let i = 0; i < p.samples.length; i++) {
  const id = p.samples[i], dt = p.timeD
Read more
Ships withnimbalyst

Nimbalyst is a free, open-source, local, interactive visual editor & session/task manager for developers, product managers, designers, builders.

Get the whole plugin