本文へ移動
cccskills
無料GitHub で公開

perf-diagnose

Diagnose Kolu client-side performance issues (memory leaks, high GPU, slow interactions) using chrome-devtools MCP + the Debug → Diagnostic info dialog + heap-snapshot analyzers. Triggers on "memory leak", "GPU memory", "tab bloat", "why is kolu slow", "debug performance", "zombie canvas", "WebGL leak", or any

インストール方法を見る

含まれるファイル(1)

  • SKILL.md16.6 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Performance diagnosis

Full historical context + fix details: docs/perf-investigations/memory-learnings.md. Master tracking issue: #610. This file is the runbook.

Ground rule: facts over guesses

Every claim must cite evidence from a live measurement: Chrome Task Manager screenshot, Diagnostic-info JSON, window.__kolu console reading, or a parsed heap snapshot. No reasoning from xterm source, no reasoning from WebGL spec, no "probably" / "likely" / "should".

Prior art: #594 regressed production to 1.1 GB GPU because its fix was reasoned from spec reading without runtime verification — the code paths being reasoned about were silently no-ops. The corresponding measurement-based fix (#596) landed in minutes once someone looked at WeakRef.deref().

If you find yourself about to write "probably" / "likely" / "I think", stop. Get the next measurement first. Hypothesis-based PR descriptions are indistinguishable from evidence-based ones three days later, including to yourself.

Ground truth: Task Manager Memory Footprint, not proxies

The ONLY metric that counts as proof of a memory-leak fix is Chrome Task Manager Memory Footprint on a fresh tab with a reproducible toggle sequence. Everything else is a proxy:

  • performance.memory.usedJSHeapSize includes uncollected garbage and can drift 3–4× from the live set.
  • system/Context / closure:* counts in a heap snapshot track SolidJS reactive-scope growth — correlate weakly with footprint. A change can reduce Context growth 89% and move footprint zero. See Chapter 3 in memory-learnings.md.
  • yn._isDisposed distribution only separates two specific leak shapes; it won't catch native / DOM / GPU retention.

Before claiming any fix works, run a fresh-tab Task Manager A/B. Open the tab, note footprint, toggle N times, note footprint. If the Δ didn't drop, the proxy you optimized wasn't the load-bearing retention.

Quiet-session A/B to isolate legitimate activity

A non-zero footprint Δ isn't automatically a leak. Agent terminals streaming output grow xterm scrollback buffers — that's the program doing its job. Before declaring a new leak:

  1. Close or idle all streaming terminals.
  2. Take the baseline on the quiet session.
  3. Run the reproducer.
  4. Compare.

#618 was filed on a +69 MB/30-toggles residual after #617. Quiet-session A/B showed 0 MB / 30 toggles — the +69 was all agent stream activity.

Prerequisites

A running dev server on http://localhost:5173 + chrome-devtools MCP. If the dev server isn't running, ask the user before starting it (AskUserQuestion): "Start just dev myself? Required to reproduce memory pathologies with the tracker attached." Don't silently run it — it ties up a terminal.

The four signals (triangulate)

  1. Chrome Task Manager (user shares screenshot). Columns: Memory Footprint, GPU Memory, JavaScript Memory total (N live). The live parenthetical is post-GC — use it, not usedJSHeapSize.
  2. Debug → Diagnostic info dialog (command palette → Debug → Diagnostic info → Copy JSON). Per-terminal state + WebGL lifecycle ledger + per-canvas sizes (#605) + per-terminal bufferBytes (#605).
  3. window.__kolu console hook (dev only; see packages/client/src/debug/consoleHooks.ts):
    • __kolu.webgl() — WebGL lifecycle snapshot
    • __kolu.bufferBytes("id") — xterm's primary + alt Uint32Array.byteLength
    • __kolu.atlas("id") — WebGL atlas dimensions
    • __kolu.lifecycle() — { mounts, cleanups } for Terminal.tsx (disposal audit)
  4. Heap snapshot via mcp__chrome-devtools__take_memory_snapshot to /tmp/kolu.heapsnapshot. Chrome forces a major GC before capturing — snapshots reflect the live set.

Three leak shapes — distinguish these first

Before digging into any specific retention chain, figure out which shape you have. They have different fixes.

ShapeHow to tellFix pattern
Cleanup doesn't run (#598 class)__kolu.lifecycle() shows mounts - cleanups > live. Orphan entry's event tape has only {kind: "create"} — no dispose event ever fires.Register onCleanup synchronously before any await; bail with if (disposed) post-await.
Cleanup runs but memory retained (#606 class)lifecycle() math works. scripts/check-yn-disposed.mjs shows all retained yn have _store._isDisposed=true. Rn count > live count.Null captured refs in component scope (#607) OR register disposables (#609).
Callback retained past dispose() (#617 class)diff-heap.mjs shows a large native class (JSArrayBufferData, Uint32Array, SVGAnimated*, etc.) growing. find-retainers.mjs traces it through a Window.<ObserverCtor> → Map/registry → callback closure → a disposed service object. Happens when the browser's observer registry (or a DevTools/extension wrapper) retains the callback closure past our explicit observer.disconnect() call.Wrap the captured this in WeakRef inside the callback. observer.disconnect() stays as-is; the WeakRef is defensive for when it doesn't fully release. See #617 for the xterm RenderService.IntersectionObserver case.

WebGL lifecycle invariants

Healthy steady-state (__kolu.webgl() or Diagnostic dialog):

  • totalCreated - disposed == aliveInDom == 1 — only the focused tile.
  • contextsLost == aliveDetached — every detached canvas's GPU released.
  • Every contextlost event in tape has defaultPrevented: false.

Violation patterns:

ViolationDiagnosis
totalCreated - disposed > 1 + orphan tape has only {kind: "create"}Async-onMount cleanup race. Look for a new await before onCleanup(...) registers. Fix pattern in #598.
aliveDetached > contextsLostloseContext() isn't firing. Check canvas selector (#596 pattern) or xterm preventDefault.
Retained yn._store._isDisposed=trueCleanup runs, memory pinned externally. Run orphan-paths.mjs to find the pin.
contextlost with defaultPrevented: true time-adjacent to active usexterm's listener ran before disposal — schedules a 3 s restoration timer.

Heap-snapshot analyzers (five durable scripts)

docs/perf-investigations/scripts/. Run as:

node --max-old-space-size=8192 script.mjs path/to/snapshot.heapsnapshot

Use in order — start with diff-heap.mjs. It runs across a baseline/post pair and names the leaking class in one line; everything after that is a targeted retainer walk on that one class.

  1. diff-heap.mjs baseline.heapsnapshot post.heapsnapshot — aggregates self_size bytes per class in each snapshot and prints the top classes by byte growth. Sort by bytes, not count: a 220 MB Uint32Array leak (#617) drowns any number of 40-byte Context churn. First tool to reach for on any retention investigation.
  2. find-retainers.mjs snap.heapsnapshot <type> <name> — BFS from GC roots to every instance of the target class, groups by retainer-path signature, prints the dominant chain. Call with the class that diff-heap.mjs flagged (e.g. native Uint32Array or closure debouncedFit). This identifies the pin site definitively.
  3. count-rn.mjs — instance-count histogram for xterm classes (Rn, Dl, Qt, etc.). Useful once you've narrowed the investigation to xterm, as a quick "do the counts look sane for the live terminal count" sanity check.
  4. check-yn-disposed.mjs — distribution of yn._store._isDisposed. Separates "cleanup doesn't run" from "cleanup runs but memory retained" (#598 vs #606). If all retained yn are true, cleanup ran and the leak is post-dispose.
  5. orphan-paths.mjs — special-case of find-retainers.mjs pre-filtered to orphan Rn (disposed-but-retained xterm Terminals). Use when you already know the leak is xterm-shaped.

Rule of thumb: sort by bytes, follow the heaviest

The dominant byte-growth class is almost always the one retaining everything else through its closure chain. Fixing a small-byte class while the heaviest is still leaking gets you zero footprint improvement. Chapter 3 in memory-learnings.md is the full cautionary tale on this.

Minified class names shift between xterm versions (e.g. Ht in one build is CursorBlinkStateManager in addon-webgl). Verify by outgoing-edge shape:

  • Rn (xterm Terminal) — has only _core + __proto__ edges; real state in _core: yn
  • yn (xterm CoreBrowserTerminal) — has _store, element, _inputHandler, many services
  • CursorBlinkStateManager — has _renderCallback, isCursorVisible, _animationTimeRestarted, _animationFrame
  • TextureAtlasPage — has a canvas property pointing at a 512×512 canvas

Reproduction recipes

Focus swaps alone rarely leak. What does:

ScenarioTriggers
30× Focus↔Canvas mode toggle with 6–7 terminalsThe canonical Chapter-3 repro. Pre-#617: +220 MB of Uint32Array (BufferLines) on native side, +367 MB Memory Footprint. Post-fix: flat.
6× Focus↔Canvas mode toggle with 4 terminalsThe canonical Chapter-2 repro. Pre-#607+#609: 24 orphans. Post-fix: 0.
Terminal close (× + confirm, or Ctrl+D)Server removal → <For> unmounts the tile → Terminal disposes.
Ctrl+Enter + Ctrl+D rapid-fireStresses the await document.fonts.load window in onMount.
20× Ctrl+Enter+Ctrl+DRegression test. Pre-#598: ~6 orphans per cycle. Post-#598: 0.

Combine over ~1–2 minutes of scripted churn before reading the dialog.

Always run the repro on a quiet session (all agent terminals idle, no streaming output) for leak measurements. Active scrollback growth from live agents will inflate the Δ by tens of MB and look indistinguishable from retention. See the quiet-session A/B section above.

Interpreting the gap (footprint - JS - GPU)

Task Manager Memory Footprint includes JS live + GPU + renderer baseline + detached DOM bitmap native memory + V8 code cache. The last bucket is invisible to both JS heap and GPU counters. Rough budget for kolu:

  • 1 live WebGL context ≈ 30 MB GPU
  • Compositor layers for N canvas tiles ≈ N × 20–30 MB GPU
  • Chrome renderer baseline ≈ 100–150 MB
  • V8 "sticky" heap (usedJSHeapSize - live) often 300–500 MB on a long-running tab — this is V8 not returning unused heap to the OS, not a leak.
  • Large canvas bitmaps (the active WebGL canvas at DPR=2 is ~15 MB) linger past JS-side disposal until Chrome's compositor recycles — native-side, invisible to our tools.

If JS heap collapses under forced GC and WebGL invariants are clean, remaining footprint is most likely benign V8/renderer retention. That's the Part 2 territory (#610): post-disposal-leak-fixes investigation.

Chrome Task Manager vs performance.memory

  • performance.memory.usedJSHeapSize (what the dialog shows) = bytes allocated, includes uncollected garbage.
  • Task Manager JavaScript Memory (N live) = live set after last major GC.

The gap is collectable garbage, not a leak. Force a major GC (DevTools → Memory → trash icon, or take a heap snapshot) and compare.

When in doubt — fork xterm.js

If retention traces to an xterm-internal class (Chapter 2 pattern), the fix is likely a 1–3 line this._register(...) wrapper in xterm source. Pattern from #609:

  1. Fork xtermjs/xterm.js → juspay/xterm.js.
  2. Apply fix to .ts source on a fix/xxx branch.
  3. Create a fix/xxx-built branch that also commits the built .mjs bundles (so pnpm can consume via github:juspay/xterm.js#fix/xxx-built without running xterm's install-time build toolchain).
  4. Use pnpm.overrides in kolu's package.json pointing at the built branch.
  5. Open upstream issue first, then upstream PR linking to it.

Once upstream merges + releases, the override collapses to a plain version bump in package.json.

Prior art (PRs / issues cited in this skill)

  • #591 — original WebGL-context accumulation report
  • #592 — diagnostic dialog foundation
  • #594 — spec-reasoning regression (closed unmerged); the "lesson" PR
  • #595 — webglTracker WeakRef ledger
  • #596 — wrong-canvas-selector fix (:not(.xterm-link-layer))
  • #598 — async-onMount cleanup race
  • #600 — runWithOwner for @solid-primitives/resize-observer
  • #605 — per-terminal bufferBytes + aliveCanvases
  • #606 — Chapter 2 tracking issue (mode-toggle retention)
  • #607 — Kolu-side fitAddon/searchAddon null-out + window.__kolu hook
  • #609 — xterm-side patch via juspay/xterm.js fork (CursorBlinkStateManager + _pausedResizeTask)
  • #610 — master tracking issue for all memory work
  • #614 — Chapter 3's wrong turn (closed unmerged); six commits chasing system/Context count proxy with no Task Manager effect
  • #617 — Chapter 3's fix: WeakRef in RenderService's IntersectionObserver callback. Production: −81% Memory Footprint growth per 30 toggles.
  • #618 — residual-leak follow-up, closed as "legitimate agent-stream activity" after quiet-session A/B
  • xtermjs/xterm.js#5817 + #5818 — upstream issue + PR for #609
  • xtermjs/xterm.js#5820 + #5821 — upstream issue + PR for #617

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

blog-post

無料

Write a kolu blog post grounded in the real build history — mine the Claude Code session logs behind a feature for the actual story (for large efforts, fan out over the transcripts with an ultracode workflow), draft it in the author's voice, and wire it into the Astro site. Use when asked to write a blog post or engineering essay about something that was built, especially one that should read as a narrative of what actually happened rather than invented marketing copy.

日本語の概要は準備中です。原文の説明を表示しています。

juspay/kolu812026年10月13日 更新

ci

無料

Run this repo's CI end-to-end — the kolu-specific procedure on top of the odu runner: the odu MCP front door, banned opt-out flags, mandatory two-platform coverage, venue-pool leasing, fail-fast surfacing, e2e timing evidence, and the green-gate. Triggers on "run CI", "drive CI", "re-run the pipeline", "close the red check", "warm the pool". Runner mechanics live in the `/odu` skill.

日本語の概要は準備中です。原文の説明を表示しています。

juspay/kolu812026年10月13日 更新

Launch the kolu dev server on two RANDOM free ports so it never collides with the running production `kolu.service`, remember the ports for the rest of the session, and tear down only the dev instance. Load before running the app locally — for evidence capture, driving a live kolu with the chrome-devtools MCP, or any `just dev` you'd otherwise run by hand. Triggers on "run kolu locally", "launch the dev server", "boot kolu", "drive a live kolu", "start the app to screenshot it", or before pointing chrome-devtools at a local kolu.

日本語の概要は準備中です。原文の説明を表示しています。

juspay/kolu812026年10月13日 更新

diataxis

無料

Classify, write, and audit documentation per Diátaxis (diataxis.fr) — the four-quadrant method (tutorials · how-to guides · reference · explanation). Encodes the compass as a decision procedure, each quadrant's contract as hard rules, and an executable mixed-mode audit. Project-agnostic; the per-repo doc-location mapping lives in a repo rule of the project's own (kolu: `.apm/instructions/diataxis.instructions.md`). Use when writing any user-facing doc, structuring a docs site, or auditing existing docs for mixed modes.

日本語の概要は準備中です。原文の説明を表示しています。

juspay/kolu812026年10月13日 更新

evidence

無料

Produce visual PR evidence — a screenshot or video — whenever a change has on-screen impact. Tests are never a substitute; a change can be backend by cause and visible by effect. Capture via the project's e2e harness on a pu box, or by driving a live kolu with the chrome-devtools MCP; host on a GitHub release and post a `## Evidence` comment. Triggers on "post evidence", "screenshot the change", "record a video of this", "show it working", "prove it", or finishing any change whose effect is visible on screen.

日本語の概要は準備中です。原文の説明を表示しています。

juspay/kolu812026年10月13日 更新

Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.

日本語の概要は準備中です。原文の説明を表示しています。

juspay/kolu812026年10月13日 更新

juspay のスキルをすべて見る

このスキルの問題を報告する