feat(tui): show live performance diagnostics
This commit is contained in:
parent
beaa51b4ff
commit
fcfb78e433
4 changed files with 134 additions and 2 deletions
|
|
@ -46,6 +46,7 @@ import { useEvent } from "./context/event"
|
|||
import { ClientProvider, useClient } from "./context/client"
|
||||
import { StartupLoading } from "./component/startup-loading"
|
||||
import { DevToolsSidebar } from "./component/devtools-sidebar"
|
||||
import { PerformanceDevTools } from "./component/performance-devtools"
|
||||
import { DevTools } from "./devtools"
|
||||
import { Reconnecting } from "./component/reconnecting"
|
||||
import { DataProvider, useData } from "./context/data"
|
||||
|
|
@ -1135,7 +1136,10 @@ function App(props: { pair?: DialogPairCredentials; started: number }) {
|
|||
</Show>
|
||||
</box>
|
||||
<Show when={devtools()}>
|
||||
<DevToolsSidebar />
|
||||
<>
|
||||
<PerformanceDevTools />
|
||||
<DevToolsSidebar />
|
||||
</>
|
||||
</Show>
|
||||
</box>
|
||||
<Show when={!startup.skipInitialLoading}>
|
||||
|
|
|
|||
93
packages/tui/src/component/performance-devtools.tsx
Normal file
93
packages/tui/src/component/performance-devtools.tsx
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import { useRenderer } from "@opentui/solid"
|
||||
import { onCleanup } from "solid-js"
|
||||
import { DevTools } from "../devtools"
|
||||
|
||||
const sampleInterval = 1_000
|
||||
const eventLoopInterval = 100
|
||||
|
||||
export function PerformanceDevTools() {
|
||||
const renderer = useRenderer()
|
||||
const runtime = DevTools.register({ id: "runtime-performance", title: "Runtime performance" })
|
||||
const rendering = DevTools.register({ id: "renderer-performance", title: "Renderer performance" })
|
||||
let previousTime = performance.now()
|
||||
let previousCpu = process.cpuUsage()
|
||||
let eventLoopTime = previousTime
|
||||
let eventLoopLag = 0
|
||||
|
||||
renderer.resetStats()
|
||||
renderer.setGatherStats(true)
|
||||
|
||||
const eventLoopTimer = setInterval(() => {
|
||||
const now = performance.now()
|
||||
eventLoopLag = Math.max(eventLoopLag, now - eventLoopTime - eventLoopInterval)
|
||||
eventLoopTime = now
|
||||
}, eventLoopInterval)
|
||||
|
||||
const sample = setInterval(() => {
|
||||
const now = performance.now()
|
||||
const cpu = process.cpuUsage()
|
||||
const elapsed = now - previousTime
|
||||
const memory = process.memoryUsage()
|
||||
const stats = renderer.getStats()
|
||||
const scheduler = renderer.getSchedulerState()
|
||||
const frameTimes = stats.frameTimes.toSorted((left, right) => left - right)
|
||||
const frameP95 = frameTimes[Math.ceil(frameTimes.length * 0.95) - 1]
|
||||
|
||||
runtime.setAll([
|
||||
{
|
||||
key: "TUI CPU",
|
||||
value: `${(((cpu.user - previousCpu.user + cpu.system - previousCpu.system) / (elapsed * 1_000)) * 100).toFixed(1)}%`,
|
||||
},
|
||||
{ key: "Event loop max", value: `${Math.max(0, eventLoopLag).toFixed(1)} ms` },
|
||||
{ key: "RSS", value: megabytes(memory.rss) },
|
||||
{ key: "Heap", value: `${megabytes(memory.heapUsed)} / ${megabytes(memory.heapTotal)}` },
|
||||
{ key: "Array buffers", value: megabytes(memory.arrayBuffers) },
|
||||
])
|
||||
rendering.setAll([
|
||||
{ key: "FPS", value: stats.fps },
|
||||
{ key: "Frame p95", value: milliseconds(frameP95) },
|
||||
{ key: "Frame max", value: milliseconds(stats.maxFrameTime || undefined) },
|
||||
{ key: "Native render", value: microseconds(stats.nativeRenderTime) },
|
||||
{ key: "Terminal write", value: microseconds(stats.nativeStdoutWriteTime) },
|
||||
{ key: "Frame callbacks", value: milliseconds(stats.frameCallbackTime) },
|
||||
{ key: "Cells updated", value: stats.cellsUpdated },
|
||||
{ key: "Cells average", value: stats.averageCellsUpdated },
|
||||
{ key: "Target FPS", value: renderer.targetFps },
|
||||
{
|
||||
key: "Scheduler",
|
||||
value: scheduler.isRendering
|
||||
? "rendering"
|
||||
: scheduler.hasScheduledRender
|
||||
? "scheduled"
|
||||
: scheduler.isRunning
|
||||
? "live"
|
||||
: "idle",
|
||||
},
|
||||
{ key: "Terminal", value: `${renderer.width} x ${renderer.height}` },
|
||||
])
|
||||
|
||||
previousTime = now
|
||||
previousCpu = cpu
|
||||
eventLoopLag = 0
|
||||
}, sampleInterval)
|
||||
|
||||
onCleanup(() => {
|
||||
clearInterval(eventLoopTimer)
|
||||
clearInterval(sample)
|
||||
renderer.setGatherStats(false)
|
||||
})
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function megabytes(bytes: number) {
|
||||
return `${(bytes / 1024 / 1024).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
function milliseconds(value: number | undefined) {
|
||||
return value === undefined ? "n/a" : `${value.toFixed(2)} ms`
|
||||
}
|
||||
|
||||
function microseconds(value: number | undefined) {
|
||||
return value === undefined ? "n/a" : milliseconds(value / 1_000)
|
||||
}
|
||||
|
|
@ -4,10 +4,12 @@ import { createSignal } from "solid-js"
|
|||
|
||||
export type Value = string | number | boolean | null
|
||||
|
||||
export type Entry = Readonly<{ key: string; value: Value }>
|
||||
|
||||
export type Group = Readonly<{
|
||||
id: string
|
||||
title: string
|
||||
entries: readonly Readonly<{ key: string; value: Value }>[]
|
||||
entries: readonly Entry[]
|
||||
}>
|
||||
|
||||
const [groups, setGroups] = createSignal<readonly Group[]>([])
|
||||
|
|
@ -35,6 +37,16 @@ export function register(input: { id: string; title: string }) {
|
|||
}),
|
||||
)
|
||||
},
|
||||
setAll(entries: readonly Entry[]) {
|
||||
setGroups((groups) =>
|
||||
groups.map((group) => {
|
||||
if (group.id !== input.id) return group
|
||||
const next = new Map(group.entries.map((entry) => [entry.key, entry]))
|
||||
entries.forEach((entry) => next.set(entry.key, entry))
|
||||
return { ...group, entries: [...next.values()] }
|
||||
}),
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,3 +17,26 @@ test("registers and updates grouped DevTools data", () => {
|
|||
],
|
||||
})
|
||||
})
|
||||
|
||||
test("updates a DevTools group in one batch", () => {
|
||||
const group = DevTools.register({ id: "batch", title: "Batch data" })
|
||||
|
||||
group.setAll([
|
||||
{ key: "CPU", value: "20%" },
|
||||
{ key: "Memory", value: "100 MB" },
|
||||
])
|
||||
group.setAll([
|
||||
{ key: "CPU", value: "30%" },
|
||||
{ key: "FPS", value: 60 },
|
||||
])
|
||||
|
||||
expect(DevTools.data().find((item) => item.id === "batch")).toEqual({
|
||||
id: "batch",
|
||||
title: "Batch data",
|
||||
entries: [
|
||||
{ key: "CPU", value: "30%" },
|
||||
{ key: "Memory", value: "100 MB" },
|
||||
{ key: "FPS", value: 60 },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue