.cpuprofile files you can analyze with profano, a CLI tool that prints the heaviest functions as a terminal table.BUN_OPTIONS env var to inject profiling flags. Bun prepends these to every execution automatically.1234BUN_OPTIONS="--cpu-prof --cpu-prof-dir=./tmp/cpu-profiles" termcast dev ./my-extension # use the TUI, then Ctrl+C to exit bunx profano ./tmp/cpu-profiles/CPU.*.cpuprofile --sort self
kill -9 skips the flush.123456789101112131415bunx tuistory launch "termcast dev ./my-extension" \ -s cpu-profile --cols 120 --rows 36 \ --env 'BUN_OPTIONS=--cpu-prof --cpu-prof-dir=./tmp/cpu-profiles' # Wait, interact, reproduce the slow behavior bunx tuistory -s cpu-profile wait "/search|commands/i" --timeout 15000 bunx tuistory -s cpu-profile press down bunx tuistory -s cpu-profile press enter # Stop bunx tuistory -s cpu-profile press ctrl c sleep 1 bunx tuistory -s cpu-profile close bunx profano ./tmp/cpu-profiles/CPU.*.cpuprofile --sort self
123456789101112131415161718192021222324252627282930313233343536373839// scripts/cpu-profile.ts import { launchTerminal } from 'tuistory' import fs from 'node:fs' import path from 'node:path' const extensionPath = process.argv[2] || '.' const profileDir = path.join(process.cwd(), 'tmp', 'cpu-profiles') fs.mkdirSync(profileDir, { recursive: true }) const session = await launchTerminal({ command: 'termcast', args: ['dev', extensionPath], cols: 120, rows: 36, env: { BUN_OPTIONS: `--cpu-prof --cpu-prof-dir=${profileDir}`, }, }) await session.waitForText('search', { timeout: 15000 }) console.log('TUI loaded') // Trigger the slow path for (let i = 0; i < 10; i++) { await session.press('down') } await session.press('enter') await session.waitIdle() // Exit await session.press(['ctrl', 'c']) await new Promise((r) => setTimeout(r, 2000)) session.close() const profiles = fs.readdirSync(profileDir).filter((f) => f.endsWith('.cpuprofile')) const latest = profiles.sort().pop() if (latest) { console.log(`\nAnalyze with: bunx profano ${path.join(profileDir, latest)} --sort self`) }
123456Duration: 12.34s Samples: 11542 active / 12340 total (6.4% idle) Self %Self Self ms Total %Total Total ms Function Location 3402 29.5% 3.40s 6804 58.9% 6.80s parseAsync src/parser.ts:142 812 7.0% 0.81s 812 7.0% 0.81s layoutYoga node_modules/@opentui/core/...
--sort self to find CPU-bound leaves (the actual hot functions). Switch to --sort total to find expensive callers that dominate wall time.PerformanceMeasure API to record what rendered, how long it took, and which file it lives in. React also emits some entries via console.timeStamp() which are not captured; this profiler collects the performance.measure() subset, which includes component renders with prop changes, scheduler phases, and mount/unmount events. Use this when you know the slowdown is in React renders and want to find which specific components are expensive.TERMCAST_REACT_PROFILE=1 and run your extension. When you exit (Ctrl+C), a .cpuprofile file is written to ./tmp/.12345TERMCAST_REACT_PROFILE=1 termcast dev ./my-extension # use the TUI, navigate around, trigger the slow behavior # then Ctrl+C to exit bunx profano ./tmp/react-profile-*.cpuprofile --sort self
1234567891011Duration: 21.25s Samples: 2267 active / 212469 total (98.9% idle) Sort: self Self %Self Self ms Total %Total Total ms Function Location 1.0ms 14.1ms Mount Components ⚛ 0.1ms 0.8ms List src/components/list.tsx:1059 0.2ms 0.7ms DescendantsProvider src/descendants.tsx:29 0.1ms 0.4ms ListSection src/components/list.tsx:2509 0.0ms 0.4ms ScrollBox src/internal/scrollbox.tsx:7 0.0ms 0.2ms ActionPanel src/components/actions.tsx:777
--sort self to find hot leaves; use --sort total to find expensive subtrees.Mount, Reconnect, and Update Blocked show the React track name instead.12345678910111213141516171819202122232425262728# 1. Launch with profiling enabled bunx tuistory launch "termcast dev ./my-extension" \ -s profile --cols 120 --rows 36 \ --env TERMCAST_REACT_PROFILE=1 # 2. Wait for the TUI to render bunx tuistory -s profile wait "/search|commands/i" --timeout 15000 # 3. Interact: navigate, open views, type in search bunx tuistory -s profile press down bunx tuistory -s profile press down bunx tuistory -s profile press enter # 4. Verify what's on screen bunx tuistory -s profile snapshot --trim # 5. Navigate back, open another view bunx tuistory -s profile press esc bunx tuistory -s profile press down bunx tuistory -s profile press enter # 6. Stop and write the profile bunx tuistory -s profile press ctrl c sleep 1 bunx tuistory -s profile close # 7. Analyze bunx profano ./tmp/react-profile-*.cpuprofile --sort total
launchTerminal API. This gives you fine-grained control over timing, lets you run the same sequence repeatedly, and integrates with test frameworks.123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657// scripts/profile-extension.ts import { launchTerminal } from 'tuistory' import fs from 'node:fs' import path from 'node:path' const extensionPath = process.argv[2] || '.' const session = await launchTerminal({ command: 'termcast', args: ['dev', extensionPath], cols: 120, rows: 36, env: { TERMCAST_REACT_PROFILE: '1' }, }) // Wait for the TUI to be ready await session.waitForText('search', { timeout: 15000 }) console.log('TUI loaded, starting interactions...') // Navigate through items to trigger renders for (let i = 0; i < 5; i++) { await session.press('down') } await session.press('enter') // Wait for the detail view to render await session.waitForText('actions', { timeout: 5000 }) console.log('Detail view rendered') // Go back and try search await session.press('esc') await session.type('test') // Wait for search results await session.waitIdle() console.log('Search complete') // Exit the TUI (Ctrl+C writes the .cpuprofile) await session.press(['ctrl', 'c']) // Give time for profile write await new Promise((r) => setTimeout(r, 2000)) session.close() // Find and report the profile file const tmpDir = path.join(process.cwd(), 'tmp') const profiles = fs.readdirSync(tmpDir).filter((f) => f.endsWith('.cpuprofile')) const latest = profiles.sort().pop() if (latest) { console.log(`\nProfile written: tmp/${latest}`) console.log(`Analyze with:`) console.log(` bunx profano tmp/${latest} --sort self`) console.log(` bunx profano tmp/${latest} --sort total`) } else { console.error('No profile file found in tmp/') }
1bun scripts/profile-extension.ts ./my-extension
performance.measure() calls in development mode with component names and timing data. The reconciler tags each measure with detail.devtools.track metadata.TERMCAST_REACT_PROFILE=1 is set, termcast installs a PerformanceObserver that collects these measures. On process exit (SIGINT, SIGTERM, or normal exit), it:.cpuprofile to ./tmp/Mount phase might have 14ms total time but only 1ms self time, with the rest distributed among child component renders.NODE_ENV must not be production)termcast dev, not compiled extensions (termcast compile sets NODE_ENV=production)PerformanceObserver must be available (Bun and Node.js 16+ both support it)12345Self %Self Self ms Total %Total Total ms Function Location ─────── ────── ─────── ─────── ────── ──────── ────────────────────── ───────────────────── 1216 53.6% 121.6ms 1216 53.6% 121.6ms Update Blocked Blocking 11 0.5% 1.1ms 172 7.6% 17.2ms Mount Components ⚛ 1 0.0% 0.1ms 8 2.7% 0.8ms List src/components/list.tsx:1059
| Column | Meaning |
| Self | Samples where this function was the leaf (exclusive time) |
| Total | Samples where this function appeared anywhere in the stack (inclusive time) |
| Function | Component name or scheduler event |
| Location | Source file path, or React track name for scheduler events |
Mount, Reconnect, Update Blocked, Cascading Update) tell you why renders happened. Cascading Update is a common performance smell; it means a render triggered another render synchronously.