Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 53 additions & 7 deletions src/components/SourceCorrelationView.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useEffect, useMemo, useState } from 'react'
import { Table, Empty } from 'antd'
import type { ColumnsType } from 'antd/es/table'
import { Resizable } from 'react-resizable'
import 'react-resizable/css/styles.css'
import { useStore } from '@/store/useStore'
import { apiFetch } from '@/api'

Expand Down Expand Up @@ -49,13 +51,49 @@ function divColor(pct: number): string {
return '#16a34a'
}

function ResizableTitle(
props: React.HTMLAttributes<HTMLElement> & {
onResize?: (e: React.SyntheticEvent, data: { size: { width: number } }) => void
width?: number
}
) {
const { onResize, width, ...restProps } = props
if (!width || !onResize) return <th {...restProps} />
return (
<Resizable
width={width}
height={0}
handle={<span className="react-resizable-handle" onClick={(e) => e.stopPropagation()} />}
onResize={onResize}
draggableOpts={{ enableUserSelectHack: false }}
>
<th {...restProps} />
</Resizable>
)
}

export default function SourceCorrelationView() {
const profileSamples = useStore((s) => s.profileSamples)
const currentSessionId = useStore((s) => s.currentSessionId)
const [selectedFuncKey, setSelectedFuncKey] = useState<string | null>(null)
const [sourceLines, setSourceLines] = useState<string[]>([])
const [disassembly, setDisassembly] = useState<Map<number, string>>(new Map())

const [colWidths, setColWidths] = useState<Record<string, number>>({
sourceLine: 70,
stallHits: 100,
stallShare: 90,
instExec: 110,
threadExec: 120,
divergencePct: 110,
coalescingFactor: 90,
})

function handleResize(key: string) {
return (_: React.SyntheticEvent, { size }: { size: { width: number } }) =>
setColWidths((prev) => ({ ...prev, [key]: Math.max(40, size.width) }))
}

const functionEntries: FunctionEntry[] = useMemo(() => {
const samples = profileSamples.filter((s) => s.sessionId === currentSessionId)
if (samples.length === 0) return []
Expand Down Expand Up @@ -190,7 +228,8 @@ export default function SourceCorrelationView() {
{
title: 'Line',
dataIndex: 'sourceLine',
width: 70,
width: colWidths.sourceLine,
onHeaderCell: () => ({ width: colWidths.sourceLine, onResize: handleResize('sourceLine') }),
render: (v: number | null) => (
<span style={{ fontFamily: 'monospace', fontSize: 12 }}>{v != null ? v : dash}</span>
),
Expand Down Expand Up @@ -237,39 +276,44 @@ export default function SourceCorrelationView() {
{
title: 'Stall Hits',
dataIndex: 'stallHits',
width: 100,
width: colWidths.stallHits,
align: 'right',
defaultSortOrder: 'descend',
sorter: (a, b) => a.stallHits - b.stallHits,
onHeaderCell: () => ({ width: colWidths.stallHits, onResize: handleResize('stallHits') }),
render: (v: number) => (v > 0 ? v.toLocaleString() : dash),
},
{
title: 'Stall %',
dataIndex: 'stallShare',
width: 90,
width: colWidths.stallShare,
align: 'right',
onHeaderCell: () => ({ width: colWidths.stallShare, onResize: handleResize('stallShare') }),
render: (v: number) => (v > 0 ? `${(v * 100).toFixed(1)}%` : dash),
},
{
title: 'Warp Instr',
dataIndex: 'instExec',
width: 110,
width: colWidths.instExec,
align: 'right',
onHeaderCell: () => ({ width: colWidths.instExec, onResize: handleResize('instExec') }),
render: (v: number) => (v > 0 ? v.toLocaleString() : dash),
},
{
title: 'Thread Instr',
dataIndex: 'threadExec',
width: 120,
width: colWidths.threadExec,
align: 'right',
onHeaderCell: () => ({ width: colWidths.threadExec, onResize: handleResize('threadExec') }),
render: (v: number) => (v > 0 ? v.toLocaleString() : dash),
},
{
title: 'Divergence',
dataIndex: 'divergencePct',
width: 110,
width: colWidths.divergencePct,
align: 'right',
sorter: (a, b) => (a.divergencePct ?? -1) - (b.divergencePct ?? -1),
onHeaderCell: () => ({ width: colWidths.divergencePct, onResize: handleResize('divergencePct') }),
render: (v: number | null) =>
v != null ? (
<span style={{ color: divColor(v), fontWeight: 600 }}>{v.toFixed(1)}%</span>
Expand All @@ -280,9 +324,10 @@ export default function SourceCorrelationView() {
{
title: 'Coal. ×',
dataIndex: 'coalescingFactor',
width: 90,
width: colWidths.coalescingFactor,
align: 'right' as const,
sorter: (a: SourceLineRow, b: SourceLineRow) => (a.coalescingFactor ?? -1) - (b.coalescingFactor ?? -1),
onHeaderCell: () => ({ width: colWidths.coalescingFactor, onResize: handleResize('coalescingFactor') }),
render: (v: number | null) =>
v != null ? (
<span style={{ color: coalColor(v), fontWeight: 600 }}>{v.toFixed(1)}×</span>
Expand Down Expand Up @@ -399,6 +444,7 @@ export default function SourceCorrelationView() {
pagination={false}
scroll={{ y: 'calc(100vh - 300px)' }}
onRow={(record) => ({ style: rowStyle(record.stallShare) })}
components={{ header: { cell: ResizableTitle } }}
/>
</>
)}
Expand Down
55 changes: 42 additions & 13 deletions src/pages/SessionList.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React, { useEffect, useState } from 'react'
import { Button, Tag, Typography, Table, Empty, Spin } from 'antd'
import { Button, Popconfirm, Tag, Typography, Table, Empty, Spin } from 'antd'
import type { ColumnsType } from 'antd/es/table'
import { useNavigate } from 'react-router-dom'
import { DesktopOutlined, RightOutlined } from '@ant-design/icons'
import { DeleteOutlined, DesktopOutlined, RightOutlined } from '@ant-design/icons'
import { useStore } from '@/store/useStore'
import { useAuthStore } from '@/store/useAuthStore'
import { SessionSummary, CudaGpuInfo } from '@/types'
import dayjs from 'dayjs'

Expand Down Expand Up @@ -76,9 +77,12 @@ const sessionColumns: ColumnsType<SessionSummary> = [
export default function SessionList() {
const hosts = useStore((s) => s.hosts)
const fetchHosts = useStore((s) => s.fetchHosts)
const deleteSession = useStore((s) => s.deleteSession)
const role = useAuthStore((s) => s.role)
const navigate = useNavigate()

const [loading, setLoading] = useState(false)
const [deletingId, setDeletingId] = useState<string | null>(null)
// selectedGpuKey: `${hostname}::${deviceId}` — null means show all sessions for the host
const [selectedGpuKey, setSelectedGpuKey] = useState<string | null>(null)
const [expandedHost, setExpandedHost] = useState<string | null>(null)
Expand All @@ -88,22 +92,47 @@ export default function SessionList() {
fetchHosts().finally(() => setLoading(false))
}, [fetchHosts])

const canDelete = role === 'ADMIN' || role === 'USER'

const viewColumn: ColumnsType<SessionSummary>[number] = {
title: '',
key: 'actions',
align: 'right' as const,
render: (_: unknown, record: SessionSummary) => (
<Button
type="primary"
size="small"
icon={<RightOutlined />}
onClick={() => {
// Also trigger init load so the dashboard has event data
navigate(`/dashboard/${record.sessionId}`)
}}
>
View
</Button>
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
<Button
type="primary"
size="small"
icon={<RightOutlined />}
onClick={() => navigate(`/dashboard/${record.sessionId}`)}
>
View
</Button>
{canDelete && (
<Popconfirm
title="Delete session"
description="All event data for this session will be permanently removed."
onConfirm={async () => {
setDeletingId(record.sessionId)
try {
await deleteSession(record.sessionId)
} finally {
setDeletingId(null)
}
}}
okText="Delete"
okButtonProps={{ danger: true }}
cancelText="Cancel"
>
<Button
danger
size="small"
icon={<DeleteOutlined />}
loading={deletingId === record.sessionId}
/>
</Popconfirm>
)}
</div>
),
}

Expand Down
6 changes: 6 additions & 0 deletions src/store/useStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ interface AppState {

// actions
fetchHosts: () => Promise<void>;
deleteSession: (sessionId: string) => Promise<void>;
fetchInit: () => Promise<void>;
fetchSystemMetrics: (sessionId: string) => Promise<void>;
fetchProfileSamples: (sessionId: string) => Promise<void>;
Expand Down Expand Up @@ -84,6 +85,11 @@ export const useStore = create<AppState>((set, get) => ({
console.error('Failed to fetch hosts', err);
}
},
deleteSession: async (sessionId: string) => {
const res = await apiFetch(`/api/v1/events/sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE' });
if (!res.ok) throw new Error(`Failed to delete session: HTTP ${res.status}`);
await get().fetchHosts();
},
fetchInit: async () => {
try {
const dateTo = new Date();
Expand Down
Loading