From e37a58bf4aae27526461b69548b351b0da1ff2d9 Mon Sep 17 00:00:00 2001 From: CodingInAVan Date: Sun, 22 Mar 2026 23:18:08 -0700 Subject: [PATCH] Optimizing schema --- src/components/SassMetricsView.tsx | 174 +++++++++------------------- src/components/ScopeView.tsx | 61 ++++++---- src/components/StallReasonChart.tsx | 26 ++++- src/pages/ApiKeysPage.tsx | 17 ++- src/store/useAuthStore.ts | 2 +- src/types.ts | 15 +-- 6 files changed, 136 insertions(+), 159 deletions(-) diff --git a/src/components/SassMetricsView.tsx b/src/components/SassMetricsView.tsx index c6afe76..550bddd 100644 --- a/src/components/SassMetricsView.tsx +++ b/src/components/SassMetricsView.tsx @@ -1,30 +1,34 @@ -import React, { useMemo, useState } from 'react' +import { useMemo, useState } from 'react' import { Table } from 'antd' import type { ColumnsType } from 'antd/es/table' import { useStore } from '@/store/useStore' -import { formatWallClock, fmtRelNs } from '@/utils/timeFormat' interface InstructionRow { key: string - pcOffset: string + pcOffset: number | null functionName: string sourceFile: string - sourceLine: number | null - tsNs: number instExecuted: number threadInstExecuted: number avgActiveThreads: number divergencePct: number } -interface KernelEntry { +interface ScopeEntry { groupKey: string - kernelName: string - startTsNs: number + scopeName: string avgActiveThreads: number rows: InstructionRow[] } +// functionName is stored as "name@sourceFile" from the client dictionary +function parseFunctionKey(raw?: string): { name: string; sourceFile: string } { + if (!raw) return { name: '', sourceFile: '' } + const at = raw.lastIndexOf('@') + if (at === -1) return { name: raw, sourceFile: '' } + return { name: raw.slice(0, at), sourceFile: raw.slice(at + 1) } +} + function divergenceColor(avg: number): string { if (avg >= 28) return '#16a34a' if (avg >= 16) return '#ca8a04' @@ -39,61 +43,31 @@ function rowBackground(avg: number): React.CSSProperties { export default function SassMetricsView() { const profileSamples = useStore((s) => s.profileSamples) - const events = useStore((s) => s.events) const currentSessionId = useStore((s) => s.currentSessionId) - const globalRange = useStore((s) => s.globalRange) - const sessionStartNs = globalRange?.start_ns const [selectedGroupKey, setSelectedGroupKey] = useState(null) - const kernelMap = useMemo(() => { - const m = new Map() - for (const e of events) { - if (e.type === 'kernel' && e.sessionId === currentSessionId) { - const corrId = (e as any).corrId ?? (e as any).corr_id - if (corrId != null) m.set(corrId, e.name) - } - } - return m - }, [events, currentSessionId]) - - const scopeMap = useMemo(() => { - const m = new Map() - for (const e of events) { - if (e.type === 'scope' && e.sessionId === currentSessionId) { - m.set(e.id, e.name) - } - } - return m - }, [events, currentSessionId]) - - const kernelEntries: KernelEntry[] = useMemo(() => { + const scopeEntries: ScopeEntry[] = useMemo(() => { const sassSamples = profileSamples.filter( (s) => s.sampleKind === 'sass_metric' && s.sessionId === currentSessionId, ) - // Group key: for SASS metrics (corrId=0), use scopeId; for PC sampling, use corrId - type PcKey = string + // Group by scopeName (resolved string stored directly in the DB row) + type PcKey = number | null const pcData = new Map< string, - Map + Map >() for (const s of sassSamples) { - const groupKey = s.corrId === 0 ? `scope:${s.scopeId ?? 'unknown'}` : `corr:${s.corrId}` - const pcOffset = s.pcOffset ?? '0x0' + const groupKey = s.scopeName ?? '(no scope)' + const pcKey = s.pcOffset ?? null if (!pcData.has(groupKey)) pcData.set(groupKey, new Map()) - const corrMap = pcData.get(groupKey)! - if (!corrMap.has(pcOffset)) { - corrMap.set(pcOffset, { - instExecuted: 0, - threadInstExecuted: 0, - functionName: s.functionName ?? '', - sourceFile: s.sourceFile ?? '', - sourceLine: s.sourceLine ?? null, - tsNs: s.tsNs ?? 0, - }) + const pcMap = pcData.get(groupKey)! + if (!pcMap.has(pcKey)) { + const { name, sourceFile } = parseFunctionKey(s.functionName) + pcMap.set(pcKey, { instExecuted: 0, threadInstExecuted: 0, functionName: name, sourceFile }) } - const entry = corrMap.get(pcOffset)! + const entry = pcMap.get(pcKey)! if (s.metricName === 'smsp__sass_inst_executed') { entry.instExecuted += s.metricValue ?? 0 } else if (s.metricName === 'smsp__sass_thread_inst_executed') { @@ -101,7 +75,7 @@ export default function SassMetricsView() { } } - const result: KernelEntry[] = [] + const result: ScopeEntry[] = [] for (const [groupKey, pcMap] of pcData.entries()) { const rows: InstructionRow[] = [] for (const [pcOffset, vals] of pcMap.entries()) { @@ -112,8 +86,6 @@ export default function SassMetricsView() { pcOffset, functionName: vals.functionName, sourceFile: vals.sourceFile, - sourceLine: vals.sourceLine, - tsNs: vals.tsNs, instExecuted: vals.instExecuted, threadInstExecuted: vals.threadInstExecuted, avgActiveThreads: avg, @@ -122,63 +94,33 @@ export default function SassMetricsView() { } rows.sort((a, b) => a.avgActiveThreads - b.avgActiveThreads) - // Weighted mean across instructions let totalInst = 0 let weightedSum = 0 for (const r of rows) { totalInst += r.instExecuted weightedSum += r.avgActiveThreads * r.instExecuted } - const sessionAvg = totalInst > 0 ? weightedSum / totalInst : 0 - - const startTsNs = rows.length > 0 ? Math.min(...rows.map((r) => r.tsNs)) : 0 - - // Resolve display name: scope name for SASS metrics, kernel name for PC sampling - let displayName: string - if (groupKey.startsWith('scope:')) { - const scopeId = groupKey.slice('scope:'.length) - displayName = scopeMap.get(scopeId) ?? `scope:${scopeId.slice(0, 8)}` - } else { - const corrId = parseInt(groupKey.slice('corr:'.length), 10) - displayName = kernelMap.get(corrId) ?? `corr_id:${corrId}` - } + const scopeAvg = totalInst > 0 ? weightedSum / totalInst : 0 - result.push({ - groupKey, - kernelName: displayName, - startTsNs, - avgActiveThreads: sessionAvg, - rows, - }) + result.push({ groupKey, scopeName: groupKey, avgActiveThreads: scopeAvg, rows }) } result.sort((a, b) => a.avgActiveThreads - b.avgActiveThreads) return result - }, [profileSamples, currentSessionId, kernelMap, scopeMap]) + }, [profileSamples, currentSessionId]) - const selectedKernel = kernelEntries.find((k) => k.groupKey === selectedGroupKey) ?? kernelEntries[0] ?? null + const selectedScope = scopeEntries.find((e) => e.groupKey === selectedGroupKey) ?? scopeEntries[0] ?? null const columns: ColumnsType = [ - { - title: 'Time', - dataIndex: 'tsNs', - width: 160, - render: (v: number) => ( - - {formatWallClock(v)} - {sessionStartNs != null && ( - - t+{fmtRelNs(v - sessionStartNs)} - - )} - - ), - }, { title: 'PC Offset', dataIndex: 'pcOffset', width: 110, - render: (v: string) => {v}, + render: (v: number | null) => ( + + {v != null ? `0x${v.toString(16)}` : '—'} + + ), }, { title: 'Function', @@ -188,18 +130,12 @@ export default function SassMetricsView() { }, { title: 'Source', - key: 'source', + dataIndex: 'sourceFile', ellipsis: true, - render: (_: unknown, record: InstructionRow) => { - const file = record.sourceFile - const line = record.sourceLine - if (!file) return - const basename = file.split('/').pop() ?? file - return ( - - {basename}{line != null ? `:${line}` : ''} - - ) + render: (v: string) => { + if (!v) return + const basename = v.split('/').pop() ?? v + return {basename} }, }, { @@ -236,7 +172,7 @@ export default function SassMetricsView() { }, ] - if (kernelEntries.length === 0) { + if (scopeEntries.length === 0) { return (
No SASS metric data for this session. Run the agent with SassMetrics engine enabled. @@ -246,7 +182,7 @@ export default function SassMetricsView() { return (
- {/* Left panel — kernel list */} + {/* Left panel — scope list */}
- {kernelEntries.map((k) => { - const isSelected = selectedGroupKey === k.groupKey || (selectedGroupKey === null && k === kernelEntries[0]) + {scopeEntries.map((entry) => { + const isSelected = selectedGroupKey === entry.groupKey || (selectedGroupKey === null && entry === scopeEntries[0]) return (
setSelectedGroupKey(k.groupKey)} + key={entry.groupKey} + onClick={() => setSelectedGroupKey(entry.groupKey)} style={{ padding: '8px 12px', cursor: 'pointer', @@ -278,9 +214,9 @@ export default function SassMetricsView() { textOverflow: 'ellipsis', whiteSpace: 'nowrap', }} - title={k.kernelName} + title={entry.scopeName} > - {k.kernelName} + {entry.scopeName}
- avg {k.avgActiveThreads.toFixed(1)} threads/warp + avg {entry.avgActiveThreads.toFixed(1)} threads/warp
-
- {formatWallClock(k.startTsNs)} - {sessionStartNs != null && ( - t+{fmtRelNs(k.startTsNs - sessionStartNs)} - )} -
) })} @@ -309,16 +239,16 @@ export default function SassMetricsView() { {/* Right panel — instruction table */}
- {selectedKernel && ( + {selectedScope && ( <>
- {selectedKernel.kernelName} -  — {selectedKernel.rows.length} instructions + {selectedScope.scopeName} +  — {selectedScope.rows.length} instructions
({ style: rowBackground(record.avgActiveThreads) })} diff --git a/src/components/ScopeView.tsx b/src/components/ScopeView.tsx index 0452559..e3525a2 100644 --- a/src/components/ScopeView.tsx +++ b/src/components/ScopeView.tsx @@ -82,10 +82,9 @@ function divergenceColor(avg: number): string { interface SassRow { key: string - pcOffset: string + pcOffset: number | null functionName: string sourceFile: string - sourceLine: number | null instExecuted: number threadInstExecuted: number avgActiveThreads: number @@ -93,23 +92,41 @@ interface SassRow { occurrenceCount: number } +function parseFunctionKey(raw?: string): { name: string; sourceFile: string } { + if (!raw) return { name: '', sourceFile: '' } + const at = raw.lastIndexOf('@') + if (at === -1) return { name: raw, sourceFile: '' } + return { name: raw.slice(0, at), sourceFile: raw.slice(at + 1) } +} + function buildSassRows(samples: ProfileSample[]): SassRow[] { - const rows: SassRow[] = [] + // Aggregate instExecuted / threadInstExecuted from metricName+metricValue per pcOffset + const agg = new Map() for (const s of samples) { - if (s.instExecuted === 0) continue - const avg = Math.min(32, s.threadInstExecuted / s.instExecuted) - const pcKey = `${s.functionName ?? ''}::${s.pcOffset ?? '0x0'}` + const pc = s.pcOffset ?? null + if (!agg.has(pc)) { + const { name, sourceFile } = parseFunctionKey(s.functionName) + agg.set(pc, { inst: 0, thread: 0, functionName: name, sourceFile, occurrenceCount: 0 }) + } + const entry = agg.get(pc)! + entry.occurrenceCount += s.occurrenceCount + if (s.metricName === 'smsp__sass_inst_executed') entry.inst += s.metricValue ?? 0 + else if (s.metricName === 'smsp__sass_thread_inst_executed') entry.thread += s.metricValue ?? 0 + } + const rows: SassRow[] = [] + for (const [pcOffset, vals] of agg.entries()) { + if (vals.inst === 0) continue + const avg = Math.min(32, vals.thread / vals.inst) rows.push({ - key: pcKey, - pcOffset: s.pcOffset ?? '0x0', - functionName: s.functionName ?? '', - sourceFile: s.sourceFile ?? '', - sourceLine: s.sourceLine ?? null, - instExecuted: s.instExecuted, - threadInstExecuted: s.threadInstExecuted, + key: `${pcOffset}`, + pcOffset, + functionName: vals.functionName, + sourceFile: vals.sourceFile, + instExecuted: vals.inst, + threadInstExecuted: vals.thread, avgActiveThreads: avg, divergencePct: ((32 - avg) / 32) * 100, - occurrenceCount: s.occurrenceCount, + occurrenceCount: vals.occurrenceCount, }) } return rows.sort((a, b) => a.avgActiveThreads - b.avgActiveThreads) @@ -120,7 +137,11 @@ const SASS_COLUMNS: ColumnsType = [ title: 'PC Offset', dataIndex: 'pcOffset', width: 100, - render: (v: string) => {v}, + render: (v: number | null) => ( + + {v != null ? `0x${v.toString(16)}` : '—'} + + ), }, { title: 'Function', @@ -130,12 +151,12 @@ const SASS_COLUMNS: ColumnsType = [ }, { title: 'Source', - key: 'source', + dataIndex: 'sourceFile', ellipsis: true, - render: (_: unknown, r: SassRow) => { - if (!r.sourceFile) return - const basename = r.sourceFile.split('/').pop() ?? r.sourceFile - return {basename}{r.sourceLine != null ? `:${r.sourceLine}` : ''} + render: (v: string) => { + if (!v) return + const basename = v.split('/').pop() ?? v + return {basename} }, }, { diff --git a/src/components/StallReasonChart.tsx b/src/components/StallReasonChart.tsx index 340e1f3..d56b2b8 100644 --- a/src/components/StallReasonChart.tsx +++ b/src/components/StallReasonChart.tsx @@ -16,7 +16,25 @@ interface StallReasonChartProps { samples: ProfileSample[] } -const MEMORY_KEYWORDS = ['MEM', 'L1', 'L2', 'TEXTURE'] +// CUPTI CUpti_ActivityPCSamplingStallReason integer → display name +const STALL_REASON_NAMES: Record = { + 0: 'Invalid', + 1: 'None', + 2: 'Instruction Fetch', + 3: 'Execution Dependency', + 4: 'Memory Dependency', + 5: 'Texture', + 6: 'Sync', + 7: 'Constant Memory', + 8: 'Pipe Busy', + 9: 'Memory Throttle', + 10: 'Branch Resolving', + 11: 'Wait', + 12: 'Barrier', + 13: 'Sleeping', +} + +const MEMORY_KEYWORDS = ['MEM', 'TEXTURE', 'L1', 'L2'] function isMemoryReason(reasonName: string): boolean { const upper = reasonName.toUpperCase() @@ -26,14 +44,14 @@ function isMemoryReason(reasonName: string): boolean { export default function StallReasonChart({ samples }: StallReasonChartProps) { const chartData = useMemo(() => { const pcSamples = samples.filter( - (s) => s.sampleKind === 'pc_sampling' && s.reasonName != null, + (s) => s.sampleKind === 'pc_sampling' && s.stallReason != null && s.stallReason !== 0 && s.stallReason !== 1, ) if (pcSamples.length === 0) return [] const counts = new Map() for (const s of pcSamples) { - const name = s.reasonName! - counts.set(name, (counts.get(name) ?? 0) + s.sampleCount) + const name = STALL_REASON_NAMES[s.stallReason!] ?? `Stall#${s.stallReason}` + counts.set(name, (counts.get(name) ?? 0) + (s.metricValue ?? s.occurrenceCount)) } const total = [...counts.values()].reduce((a, b) => a + b, 0) diff --git a/src/pages/ApiKeysPage.tsx b/src/pages/ApiKeysPage.tsx index e44fb2a..5ee88ed 100644 --- a/src/pages/ApiKeysPage.tsx +++ b/src/pages/ApiKeysPage.tsx @@ -87,11 +87,26 @@ export default function ApiKeysPage() { } const handleCopy = (text: string) => { - navigator.clipboard.writeText(text) + if (navigator.clipboard) { + navigator.clipboard.writeText(text).catch(() => copyFallback(text)) + } else { + copyFallback(text) + } setCopied(true) setTimeout(() => setCopied(false), 2000) } + const copyFallback = (text: string) => { + const el = document.createElement('textarea') + el.value = text + el.style.position = 'fixed' + el.style.opacity = '0' + document.body.appendChild(el) + el.select() + document.execCommand('copy') + document.body.removeChild(el) + } + const handleCloseNewKey = () => { setNewKey(null) setCopied(false) diff --git a/src/store/useAuthStore.ts b/src/store/useAuthStore.ts index f263c72..2bb36fe 100644 --- a/src/store/useAuthStore.ts +++ b/src/store/useAuthStore.ts @@ -9,7 +9,7 @@ interface AuthState { register: (email: string, username: string, password: string) => Promise } -const BASE_URL = 'http://192.168.254.31:8080' +const BASE_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:8080' export const useAuthStore = create((set) => ({ token: localStorage.getItem('auth_token'), diff --git a/src/types.ts b/src/types.ts index 9e437dd..8e7f672 100644 --- a/src/types.ts +++ b/src/types.ts @@ -136,22 +136,15 @@ export interface ProfileSample { id: string; sessionId: string; scopeName?: string; + deviceId?: number; sampleKind: 'sass_metric' | 'pc_sampling'; - functionName?: string; - pcOffset?: string; - sourceFile?: string; - sourceLine?: number; - instExecuted: number; - threadInstExecuted: number; + functionName?: string; // stored as "name@sourceFile" from client dict + pcOffset?: number; stallReason?: number; - reasonName?: string; - sampleCount: number; occurrenceCount: number; - corrId?: number; - scopeId?: string; - tsNs?: number; metricName?: string; metricValue?: number; + createdAt?: string; } export interface InsightDto {