/** * Profiler tool window: one tab per snapshot, and per snapshot a flame graph * (click to zoom, search highlights), the call tree (top-down) and the method * list (self / total). Double-click opens a frame's source when known. */ import { useLayoutEffect, useMemo, useRef, useState } from "react"; import clsx from "clsx"; import type { Profile } from "@/platform/ipc"; import { EmptyState, IconButton, Input, Tabs, Tree, T } from "@/ui"; import { editorService } from "@/workbench/contributions"; import { colorSlot, flameLayout, formatWeight, frameLocation, matchingFrames, share } from "./model"; import { activeSnapshot, closeSnapshot, useProfiler, type ProfilerTab } from "./store"; import styles from "./Profiler.module.css"; import { tx } from "@/platform/i18n"; const ROW = 18; function openFrame(profile: Profile, frame: number) { const f = profile.frames[frame]; if (!f?.file) return; void editorService().openFile( f.file, f.line !== null ? { selection: { line: f.line, col: 0 }, focus: true } : { focus: true }, ); } export function ProfilerTabs() { const snapshots = useProfiler((s) => s.snapshots); const activeId = useProfiler((s) => s.activeId); if (!snapshots.length) return null; return ( useProfiler.setState({ activeId: id })} onClose={closeSnapshot} items={snapshots.map((s) => ({ id: s.id, label: s.name, closable: true, tooltip: `${s.profile.profiler}, ${new Date(s.createdAt).toLocaleTimeString()}`, }))} /> ); } function FlameGraph({ profile, query }: { profile: Profile; query: string }) { const ref = useRef(null); const [width, setWidth] = useState(0); const [focus, setFocus] = useState(0); useLayoutEffect(() => { const el = ref.current; if (!el) return; const ro = new ResizeObserver(() => setWidth(el.clientWidth)); ro.observe(el); setWidth(el.clientWidth); return () => ro.disconnect(); }, []); const layout = useMemo(() => flameLayout(profile, focus, width > 0 ? 1 / width : 0.002), [profile, focus, width]); const matches = useMemo(() => matchingFrames(profile, query), [profile, query]); const unit = profile.unit; const total = profile.total ?? 0; return (
{focus !== 0 && (
setFocus(0)} /> {tx`Zoomed into ${profile.frames[profile.nodes[focus]!.frame]!.name}`}
)}
{width > 0 && layout.rects.map((r) => { const node = profile.nodes[r.node]!; const frame = profile.frames[node.frame]!; const w = r.width * width; const location = frameLocation(profile, node.frame); const label = r.node === 0 ? `all (${formatWeight(total, unit)})` : frame.name; return (
setFocus(r.node)} onDoubleClick={() => openFrame(profile, node.frame)} > {w > 28 && {label}}
); })}
); } function CallTree({ profile }: { profile: Profile }) { const total = profile.total ?? 0; const roots = useMemo(() => profile.nodes[0]?.children ?? [], [profile]); const [expanded, setExpanded] = useState | null>(null); // The heaviest path starts expanded (IDEA expands the hottest call chain). const initial = useMemo(() => { const set = new Set(); let cur = roots[0]; for (let i = 0; cur !== undefined && i < 30; i++) { set.add(String(cur)); cur = profile.nodes[cur]!.children[0]; } return set; }, [profile, roots]); return ( aria-label="Call tree" className={styles.tree} roots={roots} getKey={(n) => String(n)} getLabel={(n) => profile.frames[profile.nodes[n]!.frame]!.name} getChildren={(n) => (profile.nodes[n]!.children.length ? profile.nodes[n]!.children : undefined)} isExpandable={(n) => profile.nodes[n]!.children.length > 0} expanded={expanded ?? initial} onExpandedChange={(next) => setExpanded(next)} activateExpandable onActivate={(n) => openFrame(profile, profile.nodes[n]!.frame)} renderNode={(n) => { const node = profile.nodes[n]!; const location = frameLocation(profile, node.frame); return ( {share(node.total ?? 0, total)} {profile.frames[node.frame]!.name} {location && {location}} {formatWeight(node.total ?? 0, profile.unit)} {(node.selfWeight ?? 0) > 0 && ` · self ${formatWeight(node.selfWeight ?? 0, profile.unit)}`} ); }} /> ); } type MethodSort = "self" | "total" | "name"; const MAX_METHODS = 2000; function MethodList({ profile, query }: { profile: Profile; query: string }) { const [sort, setSort] = useState("self"); const total = profile.total ?? 0; const rows = useMemo(() => { const matches = matchingFrames(profile, query); const list = profile.methods.filter((m) => !matches || matches.has(m.frame)); const name = (f: number) => profile.frames[f]!.name; if (sort === "name") list.sort((a, b) => name(a.frame).localeCompare(name(b.frame))); else if (sort === "total") list.sort((a, b) => (b.total ?? 0) - (a.total ?? 0)); return list.slice(0, MAX_METHODS); }, [profile, query, sort]); const header = (key: MethodSort, label: string) => ( ); return (
{header("name", "Method")} {header("self", "Self")} {header("total", "Total")} {rows.map((m) => { const location = frameLocation(profile, m.frame); return ( openFrame(profile, m.frame)}> ); })}
{profile.frames[m.frame]!.name} {location && {location}} {formatWeight(m.selfWeight ?? 0, profile.unit)}{" "} {share(m.selfWeight ?? 0, total)} {formatWeight(m.total ?? 0, profile.unit)}{" "} {share(m.total ?? 0, total)}
); } export function ProfilerView() { const snapshot = useProfiler((s) => activeSnapshot(s)); const tab = useProfiler((s) => s.tab); const query = useProfiler((s) => s.query); if (!snapshot) { return ( Profile a run configuration to see where it spends its time. ); } const profile = snapshot.profile; return (
useProfiler.setState({ tab: id as ProfilerTab })} items={[ { id: "flame", label: "Flame Graph" }, { id: "tree", label: "Call Tree" }, { id: "methods", label: "Method List" }, ]} /> {profile.profiler} · {formatWeight(profile.total ?? 0, profile.unit)} {tab !== "tree" && ( useProfiler.setState({ query: e.target.value })} /> )}
{tab === "flame" && } {tab === "tree" && } {tab === "methods" && }
); }