import { callInBridge, callInBridgeWrite, createBridgeFromParsed, parseBridgeCliArgs, } from '../ws-bridge' interface ReactOptions { port?: number verbose?: boolean } interface SlowRow { displayName: string totalMs: number avgMs: number commitCount: number slowestCommit: number slowestMs: number } interface RerenderRow { displayName: string count: number renders?: number causes: Record } interface TreeRow { id: number displayName: string kind: string depth: number childCount: number } function printHelp() { console.log(` rnx perf react — inspect React component render cost in the tenant worker usage: rnx perf react profile start [--include-props] [--max-commits N] rnx perf react profile stop rnx perf react summary [--limit 10] [--json] rnx perf react slow [--limit 5] [--json] rnx perf react rerenders [--limit 5] [--json] rnx perf react tree [--depth 3] [--find ] [--json] rnx perf react why [--json] \`react summary\` is the demo-friendly shortcut: it runs both slow and rerenders in one round-trip, prints them as a single table per side, and surfaces the duration-zero warning prominently when the bundle is running without React profiler timers. examples: rnx perf react profile start rnx do tap-text Inbox rnx perf react profile stop rnx perf react summary --limit 10 rnx perf react slow --limit 10 rnx perf react rerenders --json `) } function valueOf(args: string[], flag: string): string | undefined { const idx = args.indexOf(flag) if (idx >= 0 && idx + 1 < args.length) return args[idx + 1] const prefix = `${flag}=` const found = args.find((arg) => arg.startsWith(prefix)) return found ? found.slice(prefix.length) : undefined } function numberFlag(args: string[], flag: string, fallback: number): number { const raw = valueOf(args, flag) if (raw === undefined) return fallback const value = Number(raw) return Number.isFinite(value) && value > 0 ? Math.round(value) : fallback } function formatTable(headers: string[], rows: string[][]): string { const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((row) => row[i]?.length ?? 0)), ) const line = (cells: string[]) => cells .map((cell, i) => cell.padEnd(widths[i])) .join(' ') .trimEnd() return [ line(headers), line(headers.map((h) => '-'.repeat(h.length))), ...rows.map(line), ] .map((row) => ` ${row}`) .join('\n') } function printJson(value: unknown) { console.log(JSON.stringify(value, null, 2)) } function printSlow(result: { rows?: SlowRow[] commits?: number error?: string durationWarning?: string }) { if (result.error) console.error(` warning: ${result.error}`) if (result.durationWarning) console.error(` warning: ${result.durationWarning}`) const rows = result.rows ?? [] if (rows.length === 0) { console.log(` no React render samples yet (${result.commits ?? 0} commit(s))`) return } console.log( formatTable( ['component', 'total', 'avg', 'commits', 'slowest'], rows.map((row) => [ row.displayName, `${row.totalMs.toFixed(2)}ms`, `${row.avgMs.toFixed(2)}ms`, String(row.commitCount), `#${row.slowestCommit} ${row.slowestMs.toFixed(2)}ms`, ]), ), ) } function printRerenders(result: { rows?: RerenderRow[] commits?: number error?: string }) { if (result.error) console.error(` warning: ${result.error}`) const rows = result.rows ?? [] if (rows.length === 0) { console.log(` no React render samples yet (${result.commits ?? 0} commit(s))`) return } console.log( formatTable( ['component', 'renders', 'causes'], rows.map((row) => { const causes = Object.entries(row.causes ?? {}) .filter(([, count]) => count > 0) .sort((a, b) => b[1] - a[1]) .slice(0, 4) .map(([cause, count]) => `${cause}:${count}`) .join(', ') return [row.displayName, String(row.renders ?? row.count), causes || '-'] }), ), ) } function printTree(result: { rows?: TreeRow[]; roots?: number; error?: string }) { if (result.error) console.error(` warning: ${result.error}`) const rows = result.rows ?? [] if (rows.length === 0) { console.log(` no React tree data yet (${result.roots ?? 0} root(s))`) return } for (const row of rows) { const indent = ' '.repeat(row.depth) console.log( ` ${indent}@${row.id} ${row.displayName} [${row.kind}] children=${row.childCount}`, ) } } function printWhy(result: { sample?: any; error?: string }) { if (result.error) { console.error(` ${result.error}`) return } const sample = result.sample if (!sample) { console.log(' no sample') return } console.log(` @${sample.fiberId} ${sample.displayName}`) console.log(` duration: ${Number(sample.actualDuration ?? 0).toFixed(2)}ms`) console.log(` causes: ${(sample.causes ?? []).join(', ') || '-'}`) if (sample.propDiffs?.length) { console.log(' props:') for (const diff of sample.propDiffs) { const values = 'old' in diff || 'new' in diff ? ` ${JSON.stringify(diff.old)} → ${JSON.stringify(diff.new)}` : '' console.log(` ${diff.key}${values}`) } } if (sample.parentChain?.length) { console.log(` parents: ${sample.parentChain.join(' ← ')}`) } } export async function runReact(args: string[], opts: ReactOptions): Promise { const parsed = parseBridgeCliArgs(args, { port: opts.port, stripBooleanFlags: ['--json', '--include-props', '--help', '-h'], stripValueFlags: ['--limit', '--depth', '--find', '--max-commits'], }) if (args.includes('--help') || args.includes('-h')) { printHelp() return 0 } const [first, second] = parsed.positional const json = args.includes('--json') const bridge = createBridgeFromParsed(parsed) try { if (first === 'profile' && second === 'start') { const result = await callInBridgeWrite( bridge, 'SootSim.bridges.reactProfile.start', { includeProps: args.includes('--include-props'), maxCommits: numberFlag(args, '--max-commits', 2000), }, ) if (json) printJson(result) else console.log(' React profile started') return 0 } if (first === 'profile' && second === 'stop') { const result = (await callInBridgeWrite( bridge, 'SootSim.bridges.reactProfile.stop', )) as { commits?: number; error?: string } if (json) printJson(result) else { console.log(` React profile stopped (${result.commits ?? 0} commit(s))`) if (result.error) console.error(` warning: ${result.error}`) } return result.error ? 1 : 0 } if (first === 'summary') { // run slow + rerenders in parallel against the existing bridge. // demo-friendly: one command, one table per dimension, plus a // single duration-zero warning that's easy to call out on stage. const limit = numberFlag(args, '--limit', 10) const [slowResult, rerendersResult] = (await Promise.all([ callInBridge(bridge, 'SootSim.bridges.reactProfile.slow', { limit }), callInBridge(bridge, 'SootSim.bridges.reactProfile.rerenders', { limit }), ])) as [ { rows?: SlowRow[] commits?: number error?: string durationWarning?: string }, { rows?: RerenderRow[]; commits?: number; error?: string }, ] if (json) { printJson({ commits: slowResult.commits ?? rerendersResult.commits ?? 0, slow: slowResult, rerenders: rerendersResult, }) return 0 } const commits = slowResult.commits ?? rerendersResult.commits ?? 0 console.log(` React profile summary (${commits} commit(s)):`) if (slowResult.error) console.error(` warning: ${slowResult.error}`) if (slowResult.durationWarning) { console.error(` warning: ${slowResult.durationWarning}`) } if (rerendersResult.error) { console.error(` warning: ${rerendersResult.error}`) } console.log('\n slowest renders:') printSlow(slowResult) console.log('\n most rerenders:') printRerenders(rerendersResult) // hint about the actualDuration-zero limitation when the slow // table is empty/zero — the demo-stage signal is "no timer // data, expect rerenders rows only". const slowestRow = slowResult.rows?.[0] const allZero = slowResult.rows && slowResult.rows.length > 0 ? slowResult.rows.every((r) => r.totalMs <= 0 && r.slowestMs <= 0) : false if (allZero && !slowResult.durationWarning) { console.error( `\n warning: every commit reports actualDuration=0ms — the React profiler timer is disabled in this bundle (rerender counts are still accurate; ${slowestRow?.displayName ? `top-listed: ${slowestRow.displayName}` : 'no slow signal'}).`, ) } return 0 } if (first === 'slow') { const result = (await callInBridge(bridge, 'SootSim.bridges.reactProfile.slow', { limit: numberFlag(args, '--limit', 10), })) as { rows?: SlowRow[] commits?: number error?: string durationWarning?: string } if (json) printJson(result) else printSlow(result) return 0 } if (first === 'rerenders') { const result = (await callInBridge( bridge, 'SootSim.bridges.reactProfile.rerenders', { limit: numberFlag(args, '--limit', 10) }, )) as { rows?: RerenderRow[]; commits?: number; error?: string } if (json) printJson(result) else printRerenders(result) return 0 } if (first === 'tree') { const result = (await callInBridge(bridge, 'SootSim.bridges.reactProfile.tree', { depth: numberFlag(args, '--depth', 3), find: valueOf(args, '--find'), })) as { rows?: TreeRow[]; roots?: number; error?: string } if (json) printJson(result) else printTree(result) return 0 } if (first === 'why') { const fiberId = Number(second) if (!Number.isFinite(fiberId) || fiberId <= 0) { console.error(' usage: rnx perf react why ') return 1 } const result = (await callInBridge( bridge, 'SootSim.bridges.reactProfile.why', Math.round(fiberId), )) as { sample?: unknown; error?: string } if (json) printJson(result) else printWhy(result) return result.error ? 1 : 0 } printHelp() return first ? 1 : 0 } finally { bridge.close() } }