Просмотр исходного кода

feat(tui): show live performance diagnostics

Kit Langton 3 недель назад
Родитель
Сommit
fcfb78e433

+ 5 - 1
packages/tui/src/app.tsx

@@ -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 - 0
packages/tui/src/component/performance-devtools.tsx

@@ -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)
+}

+ 13 - 1
packages/tui/src/devtools/index.ts

@@ -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()] }
+        }),
+      )
+    },
   }
 }
 

+ 23 - 0
packages/tui/test/devtools.test.ts

@@ -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 },
+    ],
+  })
+})