Files
hermes-agent/ui-tui/scripts/profile-tui.mjs

122 lines
5.1 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
/* global Buffer, console, process, setImmediate */
import inspector from 'node:inspector'
import { performance } from 'node:perf_hooks'
import React from 'react'
import { render } from '@hermes/ink'
import { AppLayout } from '../src/components/appLayout.tsx'
import { resetOverlayState } from '../src/app/overlayStore.ts'
import { resetTurnState } from '../src/app/turnStore.ts'
import { resetUiState } from '../src/app/uiStore.ts'
const session = new inspector.Session()
session.connect()
const post = (method, params = {}) => new Promise((resolve, reject) => {
session.post(method, params, (err, result) => err ? reject(err) : resolve(result))
})
const historySize = Number(process.env.HISTORY || 500)
const mountedRows = Number(process.env.MOUNTED || 120)
class Sink {
columns = Number(process.env.COLS || 120)
rows = Number(process.env.ROWS || 42)
isTTY = true
bytes = 0
writes = 0
listeners = new Map()
write(chunk) {
this.bytes += Buffer.byteLength(String(chunk ?? ''))
this.writes++
return true
}
on(event, fn) { this.listeners.set(event, fn); return this }
off(event) { this.listeners.delete(event); return this }
once(event, fn) { this.listeners.set(event, fn); return this }
removeListener(event) { this.listeners.delete(event); return this }
}
const theme = {
brand: { prompt: '' },
color: {
amber: '#d19a66', bronze: '#8b6f47', dim: '#6b7280', error: '#ff5555', gold: '#ffd166', label: '#61afef',
ok: '#98c379', warn: '#e5c07b', cornsilk: '#fff8dc', prompt: '#c678dd', shellDollar: '#98c379',
statusCritical: '#ff5555', statusBad: '#e06c75', statusWarn: '#e5c07b', statusGood: '#98c379',
selectionBg: '#44475a'
}
}
const noop = () => {}
const historyItems = [
{ kind: 'intro', role: 'system', text: '', info: { model: 'test', tools: {}, skills: {}, version: 'test' } },
...Array.from({ length: historySize }, (_, i) => ({
role: i % 5 === 0 ? 'user' : 'assistant',
text: `message ${i}\n${'lorem ipsum '.repeat(80)}`
}))
]
const scrollRef = { current: {
getScrollTop: () => 0,
getPendingDelta: () => 0,
getScrollHeight: () => historySize * 4,
getViewportHeight: () => 30,
getViewportTop: () => 0,
isSticky: () => true,
subscribe: () => () => {},
scrollBy: noop,
scrollTo: noop,
scrollToBottom: noop,
setClampBounds: noop,
getLastManualScrollAt: () => 0
} }
const baseProps = streamingText => ({
actions: { answerApproval: noop, answerClarify: noop, answerSecret: noop, answerSudo: noop, onModelSelect: noop, resumeById: noop, setStickyPrompt: noop },
composer: { cols: 120, compIdx: 0, completions: [], empty: false, handleTextPaste: () => null, input: '', inputBuf: [], pagerPageSize: 10, queueEditIdx: null, queuedDisplay: [], submit: noop, updateInput: noop },
mouseTracking: false,
progress: {
activity: [], outcome: '', reasoning: streamingText, reasoningActive: true, reasoningStreaming: true,
reasoningTokens: Math.ceil(streamingText.length / 4), showProgressArea: true, showStreamingArea: true,
streamPendingTools: [], streamSegments: [], streaming: streamingText, subagents: [], toolTokens: 0, tools: [], turnTrail: [], todos: []
},
status: { cwdLabel: '~/repo', goodVibesTick: 0, sessionStartedAt: Date.now(), showStickyPrompt: false, statusColor: theme.color.ok, stickyPrompt: '', turnStartedAt: Date.now(), voiceLabel: 'voice off' },
transcript: {
historyItems,
scrollRef,
virtualHistory: { bottomSpacer: 0, end: historyItems.length, measureRef: () => noop, offsets: historyItems.map((_, i) => i * 4), start: Math.max(0, historyItems.length - mountedRows), topSpacer: 0 },
virtualRows: historyItems.map((msg, index) => ({ index, key: `m${index}`, msg }))
}
})
async function main() {
resetUiState()
resetTurnState()
resetOverlayState()
const stdout = new Sink()
const stdin = { isTTY: true, setRawMode: noop, on: noop, off: noop, resume: noop, pause: noop }
const text = Array.from({ length: Number(process.env.LINES || 1200) }, (_, i) => `stream line ${i} ${'x'.repeat(90)}`).join('\n')
const inst = render(React.createElement(AppLayout, baseProps('')), { stdout, stdin, stderr: stdout, debug: false, exitOnCtrlC: false })
await post('Profiler.enable')
await post('HeapProfiler.enable')
await post('Profiler.start')
const startMem = process.memoryUsage()
const t0 = performance.now()
const iterations = Number(process.env.ITERS || 40)
for (let i = 1; i <= iterations; i++) {
const prefix = text.slice(0, Math.floor(text.length * i / iterations))
inst.rerender(React.createElement(AppLayout, baseProps(prefix)))
await new Promise(r => setImmediate(r))
}
const elapsed = performance.now() - t0
const prof = await post('Profiler.stop')
const endMem = process.memoryUsage()
await post('HeapProfiler.collectGarbage')
const afterGc = process.memoryUsage()
inst.unmount()
session.disconnect()
console.log(JSON.stringify({ elapsedMs: Math.round(elapsed), stdoutBytes: stdout.bytes, stdoutWrites: stdout.writes, startMem, endMem, afterGc, profileNodes: prof.profile.nodes.length }, null, 2))
}
main().catch(err => { console.error(err); process.exit(1) })