diff --git a/bun.lockb b/bun.lockb index 4df1f6b..ed5a58e 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/examples/example6-render-logs.fixture.tsx b/examples/example6-render-logs.fixture.tsx new file mode 100644 index 0000000..49e2838 --- /dev/null +++ b/examples/example6-render-logs.fixture.tsx @@ -0,0 +1,21 @@ +import { RunFrame } from "lib/components/RunFrame" +import React from "react" + +export default () => ( + + + + + +) +`, + }} + defaultActiveTab="render_log" + entrypoint="main.tsx" + // showRenderLogTab + /> +) diff --git a/examples/example7-large-led-matrix.fixture.tsx b/examples/example7-large-led-matrix.fixture.tsx new file mode 100644 index 0000000..725268f --- /dev/null +++ b/examples/example7-large-led-matrix.fixture.tsx @@ -0,0 +1,20 @@ +import { RunFrame } from "lib/components/RunFrame" +import React from "react" + +export default () => ( + +) +`, + "manual-edits.json": "{}", + }} + defaultActiveTab="render_log" + entrypoint="main.tsx" + // showRenderLogTab + /> +) diff --git a/lib/components/CircuitJsonPreview.tsx b/lib/components/CircuitJsonPreview.tsx index 7209009..798db26 100644 --- a/lib/components/CircuitJsonPreview.tsx +++ b/lib/components/CircuitJsonPreview.tsx @@ -8,7 +8,7 @@ import { cn } from "lib/utils" import { applyPcbEditEvents } from "lib/utils/pcbManualEditEventHandler" import { CadViewer } from "@tscircuit/3d-viewer" import { PCBViewer } from "@tscircuit/pcb-viewer" -import { useEffect, useState } from "react" +import { useCallback, useEffect, useState } from "react" import { ErrorFallback } from "./ErrorFallback" import { ErrorBoundary } from "react-error-boundary" import { ErrorTabContent } from "./ErrorTabContent" @@ -34,6 +34,19 @@ import { Button } from "./ui/button" import { PcbViewerWithContainerHeight } from "./PcbViewerWithContainerHeight" import { useStyles } from "lib/hooks/use-styles" import type { ManualEditEvent } from "@tscircuit/props" +import type { RenderLog } from "lib/render-logging/RenderLog" +import { RenderLogViewer } from "./RenderLogViewer" + +export type TabId = + | "code" + | "pcb" + | "schematic" + | "assembly" + | "cad" + | "bom" + | "circuitjson" + | "error" + | "render_log" export interface PreviewContentProps { code?: string @@ -45,6 +58,7 @@ export interface PreviewContentProps { circuitJsonKey?: string className?: string showCodeTab?: boolean + showRenderLogTab?: boolean codeTabContent?: React.ReactNode showJsonTab?: boolean showImportAndFormatButtons?: boolean @@ -62,18 +76,14 @@ export interface PreviewContentProps { hasCodeChangedSinceLastRun?: boolean // onManualEditsFileContentChange?: (newmanualEditsFileContent: string) => void - defaultActiveTab?: - | "code" - | "pcb" - | "schematic" - | "assembly" - | "cad" - | "bom" - | "circuitjson" - | "error" + defaultActiveTab?: TabId + + renderLog?: RenderLog | null onEditEvent?: (editEvent: ManualEditEvent) => void editEvents?: ManualEditEvent[] + + onActiveTabChange?: (tab: TabId) => any } export const CircuitJsonPreview = ({ @@ -86,6 +96,9 @@ export const CircuitJsonPreview = ({ showCodeTab = false, codeTabContent, showJsonTab = true, + showRenderLogTab = true, + onActiveTabChange, + renderLog, showImportAndFormatButtons = true, className, headerClassName, @@ -101,7 +114,15 @@ export const CircuitJsonPreview = ({ defaultActiveTab, }: PreviewContentProps) => { useStyles() - const [activeTab, setActiveTab] = useState(defaultActiveTab ?? "pcb") + + const [activeTab, setActiveTabState] = useState(defaultActiveTab ?? "pcb") + const setActiveTab = useCallback( + (tab: TabId) => { + setActiveTabState(tab) + onActiveTabChange?.(tab) + }, + [onActiveTabChange], + ) useEffect(() => { if (errorMessage) { @@ -229,6 +250,15 @@ export const CircuitJsonPreview = ({ /> JSON + setActiveTab("render_log")}> + + Render Log + @@ -247,7 +277,6 @@ export const CircuitJsonPreview = ({
{codeTabContent}
)} -
-
)} + {showRenderLogTab && ( + + + + )}
diff --git a/lib/components/RenderLogViewer.tsx b/lib/components/RenderLogViewer.tsx new file mode 100644 index 0000000..fba1060 --- /dev/null +++ b/lib/components/RenderLogViewer.tsx @@ -0,0 +1,39 @@ +import type { RenderLog } from "lib/render-logging/RenderLog" + +export const RenderLogViewer = ({ + renderLog, +}: { renderLog?: RenderLog | null }) => { + if (!renderLog) + return ( +
+ No render log, make sure this tab is open when you render (TODO add a + rerender button here) +
+ ) + + const orderedPhaseTimings = Object.entries( + renderLog?.phaseTimings ?? {}, + ).sort((a, b) => b[1] - a[1]) + + return ( +
+
Render Logs
+ + + + + + + + + {orderedPhaseTimings.map(([phase, duration]) => ( + + + + + ))} + +
PhaseDuration (ms)
{phase}{duration}
+
+ ) +} diff --git a/lib/components/RunFrame.tsx b/lib/components/RunFrame.tsx index 375311e..72c532e 100644 --- a/lib/components/RunFrame.tsx +++ b/lib/components/RunFrame.tsx @@ -1,5 +1,5 @@ import { createCircuitWebWorker } from "@tscircuit/eval-webworker" -import { CircuitJsonPreview } from "./CircuitJsonPreview" +import { CircuitJsonPreview, type TabId } from "./CircuitJsonPreview" import { useEffect, useRef, useState } from "react" import Debug from "debug" @@ -14,6 +14,8 @@ import evalWebWorkerBlobUrl from "@tscircuit/eval-webworker/blob-url" import type { ManualEditEvent } from "@tscircuit/props" import { useRunFrameStore } from "./RunFrameWithApi/store" import { getChangesBetweenFsMaps } from "../utils/getChangesBetweenFsMaps" +import type { RenderLog } from "lib/render-logging/RenderLog" +import { getPhaseTimingsFromRenderEvents } from "lib/render-logging/getPhaseTimingsFromRenderEvents" interface Props { /** @@ -76,6 +78,10 @@ interface Props { * If true, turns on debug logging */ debug?: boolean + + defaultActiveTab?: Parameters< + typeof CircuitJsonPreview + >[0]["defaultActiveTab"] } export const RunFrame = (props: Props) => { @@ -89,6 +95,10 @@ export const RunFrame = (props: Props) => { error?: string stack?: string } | null>(null) + const [renderLog, setRenderLog] = useState(null) + const [activeTab, setActiveTab] = useState( + props.defaultActiveTab ?? "pcb", + ) useEffect(() => { if (props.debug) Debug.enable("run-frame*") }, [props.debug]) @@ -124,6 +134,7 @@ export const RunFrame = (props: Props) => { async function runWorker() { debug("running render worker") setError(null) + const renderLog: RenderLog = {} const worker: Awaited> = globalThis.runFrameWorker ?? (await createCircuitWebWorker({ @@ -144,6 +155,15 @@ export const RunFrame = (props: Props) => { return } + if (activeTab === "render_log") { + worker.on("renderable:renderLifecycle:anyEvent", (event: any) => { + renderLog.renderEvents = renderLog.renderEvents ?? [] + event.createdAt = Date.now() + renderLog.renderEvents.push(event) + setRenderLog(renderLog) + }) + } + const evalResult = await worker .executeWithFsMap({ entrypoint: props.entrypoint, @@ -182,15 +202,24 @@ export const RunFrame = (props: Props) => { props.onCircuitJsonChange?.(circuitJson) setCircuitJson(circuitJson) props.onRenderFinished?.({ circuitJson }) + + if (activeTab === "render_log") { + renderLog.phaseTimings = getPhaseTimingsFromRenderEvents( + renderLog.renderEvents ?? [], + ) + setRenderLog(renderLog) + } } runWorker() }, [props.fsMap, props.entrypoint]) return ( +} diff --git a/lib/render-logging/getPhaseTimingsFromRenderEvents.ts b/lib/render-logging/getPhaseTimingsFromRenderEvents.ts new file mode 100644 index 0000000..66d0bda --- /dev/null +++ b/lib/render-logging/getPhaseTimingsFromRenderEvents.ts @@ -0,0 +1,47 @@ +type RenderEvent = { + type: + | `renderable:renderLifecycle:${string}:start` + | `renderable:renderLifecycle:${string}:end` + /** + * Corresponds to the element that was rendered + */ + renderId: string + createdAt: number +} +/** + * Given a list of render events, return a map of how much time was spent in each + * render phase. + * + * To get the time spent in each phase, you have to find the end event for each + * start event and subtract the createdAt of the start event from the createdAt + */ +export const getPhaseTimingsFromRenderEvents = ( + renderEvents: RenderEvent[], +): Record => { + const phaseTimings: Record = {} + if (!renderEvents) return phaseTimings + + // Create a map to store start events by phase and renderId + const startEvents = new Map() + + for (const event of renderEvents) { + const [, , phase, eventType] = event.type.split(":") + + // For start events, store them in the map keyed by phase+renderId + if (eventType === "start") { + startEvents.set(`${phase}:${event.renderId}`, event) + continue + } + + // For end events, find matching start event and calculate duration + if (eventType === "end") { + const startEvent = startEvents.get(`${phase}:${event.renderId}`) + if (startEvent) { + const duration = event.createdAt - startEvent.createdAt + phaseTimings[phase] = (phaseTimings[phase] || 0) + duration + } + } + } + + return phaseTimings +} diff --git a/package.json b/package.json index 391e2f0..608dd26 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "@tscircuit/3d-viewer": "^0.0.86", "@tscircuit/assembly-viewer": "^0.0.1", "@tscircuit/core": "^0.0.254", - "@tscircuit/eval-webworker": "^0.0.52", + "@tscircuit/eval-webworker": "^0.0.53", "@tscircuit/file-server": "^0.0.13", "@tscircuit/pcb-viewer": "^1.10.22", "@tscircuit/props": "^0.0.128",