perfViz/src/components/PhaseViewer.tsx
Michael Mainguy aa6e29fb0c Fix TypeScript build errors and improve code quality
- Remove unused variables and imports across components
- Fix BabylonJS material property errors (hasAlpha → useAlphaFromDiffuseTexture)
- Resolve TypeScript interface extension issues in PhaseViewer
- Add null safety checks for potentially undefined properties
- Ensure proper array initialization before operations
- Clean up unused function declarations

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-11 11:29:10 -05:00

540 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useMemo } from 'react'
import { useDatabaseTraceData } from '../hooks/useDatabaseTraceData'
import type { TraceEventPhase } from '../../types/trace'
interface ExtendedTraceEvent {
args: Record<string, unknown>
cat: string
name: string
ph: TraceEventPhase
pid: number
tid: number
ts: number
tts?: number
dur?: number
tdur?: number
}
const PHASE_DESCRIPTIONS: Record<string, string> = {
'M': 'Metadata',
'X': 'Complete Events',
'I': 'Instant Events',
'B': 'Begin Events',
'E': 'End Events',
'D': 'Deprecated',
'b': 'Nestable Start',
'e': 'Nestable End',
'n': 'Nestable Instant',
'S': 'Async Start',
'T': 'Async Instant',
'F': 'Async End',
'P': 'Sample',
'C': 'Counter',
'R': 'Mark'
}
const ITEMS_PER_PAGE = 50
// Helper functions to extract valuable fields
const getURL = (event: ExtendedTraceEvent): string | null => {
const args = event.args as any
return args?.url || args?.beginData?.url || args?.data?.frames?.[0]?.url || null
}
const getStackTrace = (event: ExtendedTraceEvent): any[] | null => {
const args = event.args as any
return args?.beginData?.stackTrace || null
}
const getScriptInfo = (event: ExtendedTraceEvent): { contextId?: number, scriptId?: number } => {
const args = event.args as any
return {
contextId: args?.data?.executionContextId,
scriptId: args?.data?.scriptId
}
}
const getLayoutInfo = (event: ExtendedTraceEvent): { dirtyObjects?: number, totalObjects?: number, elementCount?: number } => {
const args = event.args as any
return {
dirtyObjects: args?.beginData?.dirtyObjects,
totalObjects: args?.beginData?.totalObjects,
elementCount: args?.elementCount
}
}
const getSampleTraceId = (event: ExtendedTraceEvent): number | null => {
const args = event.args as any
return args?.beginData?.sampleTraceId || null
}
interface PhaseViewerProps {
traceId: string | null
}
export default function PhaseViewer({ traceId }: PhaseViewerProps) {
const { traceData, loading, error, stats } = useDatabaseTraceData(traceId)
const [selectedPhase, setSelectedPhase] = useState<string>('all')
const [currentPage, setCurrentPage] = useState(1)
const [searchTerm, setSearchTerm] = useState('')
const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set())
const filteredEvents = useMemo(() => {
if (!traceData) return []
let events = traceData.traceEvents
// Filter by phase
if (selectedPhase !== 'all') {
events = events.filter(event => event.ph === selectedPhase)
}
// Filter by search term
if (searchTerm) {
const term = searchTerm.toLowerCase()
events = events.filter(event =>
event.name.toLowerCase().includes(term) ||
event.cat.toLowerCase().includes(term) ||
event.pid.toString().includes(term) ||
event.tid.toString().includes(term)
)
}
return events
}, [traceData, selectedPhase, searchTerm])
const paginatedEvents = useMemo(() => {
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE
const endIndex = startIndex + ITEMS_PER_PAGE
return filteredEvents.slice(startIndex, endIndex)
}, [filteredEvents, currentPage])
const totalPages = Math.ceil(filteredEvents.length / ITEMS_PER_PAGE)
const formatTimestamp = (ts: number) => {
return (ts / 1000).toFixed(3) + 'ms'
}
const formatDuration = (dur?: number) => {
if (!dur) return '-'
return (dur / 1000).toFixed(3) + 'ms'
}
const formatThreadTime = (tts?: number) => {
if (!tts) return '-'
return (tts / 1000).toFixed(3) + 'ms'
}
const toggleRowExpansion = (eventKey: string) => {
const newExpanded = new Set(expandedRows)
if (newExpanded.has(eventKey)) {
newExpanded.delete(eventKey)
} else {
newExpanded.add(eventKey)
}
setExpandedRows(newExpanded)
}
const truncateText = (text: string, maxLength: number = 50) => {
if (text.length <= maxLength) return text
return text.substring(0, maxLength) + '...'
}
if (loading) {
return (
<div style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
minHeight: '400px',
fontSize: '18px',
color: '#6c757d'
}}>
<div style={{
width: '50px',
height: '50px',
border: '4px solid #f3f3f3',
borderTop: '4px solid #007bff',
borderRadius: '50%',
animation: 'spin 1s linear infinite',
marginBottom: '20px'
}} />
<div>Loading trace events...</div>
</div>
)
}
if (error) {
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<div style={{
background: '#f8d7da',
color: '#721c24',
padding: '15px',
borderRadius: '8px',
border: '1px solid #f5c6cb'
}}>
<h3>Error Loading Trace Data</h3>
<p>{error}</p>
</div>
</div>
)
}
if (!traceData || !stats) {
return <div style={{ padding: '20px', textAlign: 'center' }}>No trace data available</div>
}
const phaseOptions = Object.keys(stats.eventsByPhase).sort()
return (
<div style={{ padding: '20px', fontFamily: 'system-ui, sans-serif' }}>
<h2>Phase Event Viewer</h2>
{/* Controls */}
<div style={{
background: '#f8f9fa',
padding: '20px',
borderRadius: '8px',
marginBottom: '20px',
display: 'flex',
flexWrap: 'wrap',
gap: '15px',
alignItems: 'center'
}}>
{/* Phase Filter */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '5px' }}>
<label style={{ fontWeight: 'bold', fontSize: '14px' }}>Filter by Phase:</label>
<select
value={selectedPhase}
onChange={(e) => {
setSelectedPhase(e.target.value)
setCurrentPage(1)
}}
style={{
padding: '8px 12px',
border: '1px solid #ced4da',
borderRadius: '4px',
fontSize: '14px',
minWidth: '200px'
}}
>
<option value="all">All Phases ({stats.totalEvents.toLocaleString()})</option>
{phaseOptions.map(phase => (
<option key={phase} value={phase}>
{phase} - {PHASE_DESCRIPTIONS[phase] || 'Unknown'} ({stats.eventsByPhase[phase].toLocaleString()})
</option>
))}
</select>
</div>
{/* Search */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '5px' }}>
<label style={{ fontWeight: 'bold', fontSize: '14px' }}>Search:</label>
<input
type="text"
placeholder="Search by name, category, PID, or TID..."
value={searchTerm}
onChange={(e) => {
setSearchTerm(e.target.value)
setCurrentPage(1)
}}
style={{
padding: '8px 12px',
border: '1px solid #ced4da',
borderRadius: '4px',
fontSize: '14px',
minWidth: '300px'
}}
/>
</div>
{/* Results count */}
<div style={{
marginLeft: 'auto',
fontSize: '14px',
color: '#6c757d',
fontWeight: 'bold'
}}>
{filteredEvents.length.toLocaleString()} events found
</div>
</div>
{/* Pagination Controls */}
{totalPages > 1 && (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
gap: '10px',
marginBottom: '20px'
}}>
<button
onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
disabled={currentPage === 1}
style={{
padding: '8px 16px',
border: '1px solid #ced4da',
borderRadius: '4px',
background: currentPage === 1 ? '#e9ecef' : '#fff',
cursor: currentPage === 1 ? 'not-allowed' : 'pointer'
}}
>
Previous
</button>
<span style={{ margin: '0 15px', fontSize: '14px' }}>
Page {currentPage} of {totalPages}
</span>
<button
onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
disabled={currentPage === totalPages}
style={{
padding: '8px 16px',
border: '1px solid #ced4da',
borderRadius: '4px',
background: currentPage === totalPages ? '#e9ecef' : '#fff',
cursor: currentPage === totalPages ? 'not-allowed' : 'pointer'
}}
>
Next
</button>
</div>
)}
{/* Events Table */}
<div style={{
background: 'white',
border: '1px solid #dee2e6',
borderRadius: '8px',
overflow: 'hidden'
}}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ background: '#f8f9fa' }}>
<th style={{ padding: '8px', textAlign: 'center', borderBottom: '1px solid #dee2e6', fontSize: '12px', width: '30px' }}>Expand</th>
<th style={{ padding: '8px', textAlign: 'center', borderBottom: '1px solid #dee2e6', fontSize: '12px', width: '50px' }}>Phase</th>
<th style={{ padding: '8px', textAlign: 'left', borderBottom: '1px solid #dee2e6', fontSize: '12px' }}>Name</th>
<th style={{ padding: '8px', textAlign: 'left', borderBottom: '1px solid #dee2e6', fontSize: '12px' }}>Category</th>
<th style={{ padding: '8px', textAlign: 'right', borderBottom: '1px solid #dee2e6', fontSize: '12px' }}>Duration</th>
<th style={{ padding: '8px', textAlign: 'right', borderBottom: '1px solid #dee2e6', fontSize: '12px' }}>Thread Time</th>
<th style={{ padding: '8px', textAlign: 'left', borderBottom: '1px solid #dee2e6', fontSize: '12px' }}>URL</th>
<th style={{ padding: '8px', textAlign: 'right', borderBottom: '1px solid #dee2e6', fontSize: '12px' }}>PID</th>
<th style={{ padding: '8px', textAlign: 'right', borderBottom: '1px solid #dee2e6', fontSize: '12px' }}>TID</th>
</tr>
</thead>
<tbody>
{paginatedEvents.map((event, index) => {
const eventKey = `${event.pid}-${event.tid}-${event.ts}-${index}`
const isExpanded = expandedRows.has(eventKey)
const url = getURL(event as ExtendedTraceEvent)
const stackTrace = getStackTrace(event as ExtendedTraceEvent)
const layoutInfo = getLayoutInfo(event as ExtendedTraceEvent)
const scriptInfo = getScriptInfo(event as ExtendedTraceEvent)
const sampleTraceId = getSampleTraceId(event as ExtendedTraceEvent)
return (
<>
<tr key={eventKey} style={{
borderBottom: '1px solid #f1f3f4',
cursor: 'pointer'
}}
onClick={() => toggleRowExpansion(eventKey)}
>
<td style={{
padding: '8px',
textAlign: 'center',
fontSize: '12px',
color: '#007bff',
fontWeight: 'bold'
}}>
{isExpanded ? '' : '+'}
</td>
<td style={{
padding: '8px',
fontSize: '12px',
fontFamily: 'monospace',
background: getPhaseColor(event.ph),
color: 'white',
fontWeight: 'bold',
textAlign: 'center'
}}>
{event.ph}
</td>
<td style={{ padding: '8px', fontSize: '12px', maxWidth: '200px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{event.name}
</td>
<td style={{ padding: '8px', fontSize: '12px', color: '#6c757d', maxWidth: '120px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{event.cat}
</td>
<td style={{ padding: '8px', fontSize: '12px', textAlign: 'right', fontFamily: 'monospace' }}>
{formatDuration((event as ExtendedTraceEvent).dur)}
</td>
<td style={{ padding: '8px', fontSize: '12px', textAlign: 'right', fontFamily: 'monospace' }}>
{formatThreadTime((event as ExtendedTraceEvent).tts)}
</td>
<td style={{ padding: '8px', fontSize: '11px', color: '#007bff', maxWidth: '200px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{url ? (
<a href={url} target="_blank" rel="noopener noreferrer" style={{ color: '#007bff', textDecoration: 'none' }}>
{truncateText(url, 40)}
</a>
) : '-'}
</td>
<td style={{ padding: '8px', fontSize: '12px', textAlign: 'right', fontFamily: 'monospace' }}>
{event.pid}
</td>
<td style={{ padding: '8px', fontSize: '12px', textAlign: 'right', fontFamily: 'monospace' }}>
{event.tid}
</td>
</tr>
{/* Expanded Row Details */}
{isExpanded && (
<tr key={`${eventKey}-expanded`}>
<td colSpan={9} style={{
padding: '15px',
background: '#f8f9fa',
border: '1px solid #e9ecef'
}}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: '15px' }}>
{/* Timing Information */}
<div style={{ background: 'white', padding: '10px', borderRadius: '4px', border: '1px solid #dee2e6' }}>
<h4 style={{ margin: '0 0 8px 0', color: '#495057', fontSize: '14px' }}>Timing</h4>
<div style={{ fontSize: '12px', display: 'flex', flexDirection: 'column', gap: '4px' }}>
<div><strong>Timestamp:</strong> {formatTimestamp(event.ts)}</div>
<div><strong>Duration:</strong> {formatDuration((event as ExtendedTraceEvent).dur)}</div>
<div><strong>Thread Duration:</strong> {formatDuration((event as ExtendedTraceEvent).tdur)}</div>
<div><strong>Thread Time:</strong> {formatThreadTime((event as ExtendedTraceEvent).tts)}</div>
{sampleTraceId && <div><strong>Sample Trace ID:</strong> {sampleTraceId}</div>}
</div>
</div>
{/* Script/Context Information */}
{(scriptInfo.contextId || scriptInfo.scriptId) && (
<div style={{ background: 'white', padding: '10px', borderRadius: '4px', border: '1px solid #dee2e6' }}>
<h4 style={{ margin: '0 0 8px 0', color: '#495057', fontSize: '14px' }}>Script Context</h4>
<div style={{ fontSize: '12px', display: 'flex', flexDirection: 'column', gap: '4px' }}>
{scriptInfo.contextId && <div><strong>Execution Context:</strong> {scriptInfo.contextId}</div>}
{scriptInfo.scriptId && <div><strong>Script ID:</strong> {scriptInfo.scriptId}</div>}
</div>
</div>
)}
{/* Layout Information */}
{(layoutInfo.dirtyObjects || layoutInfo.totalObjects || layoutInfo.elementCount) && (
<div style={{ background: 'white', padding: '10px', borderRadius: '4px', border: '1px solid #dee2e6' }}>
<h4 style={{ margin: '0 0 8px 0', color: '#495057', fontSize: '14px' }}>Layout Metrics</h4>
<div style={{ fontSize: '12px', display: 'flex', flexDirection: 'column', gap: '4px' }}>
{layoutInfo.dirtyObjects && <div><strong>Dirty Objects:</strong> {layoutInfo.dirtyObjects}</div>}
{layoutInfo.totalObjects && <div><strong>Total Objects:</strong> {layoutInfo.totalObjects}</div>}
{layoutInfo.elementCount && <div><strong>Element Count:</strong> {layoutInfo.elementCount}</div>}
</div>
</div>
)}
{/* Stack Trace */}
{stackTrace && stackTrace.length > 0 && (
<div style={{
background: 'white',
padding: '10px',
borderRadius: '4px',
border: '1px solid #dee2e6',
gridColumn: '1 / -1'
}}>
<h4 style={{ margin: '0 0 8px 0', color: '#495057', fontSize: '14px' }}>
Stack Trace ({stackTrace.length} frames)
</h4>
<div style={{
maxHeight: '200px',
overflowY: 'auto',
fontSize: '11px',
fontFamily: 'monospace',
background: '#f8f9fa',
padding: '8px',
borderRadius: '3px'
}}>
{stackTrace.slice(0, 10).map((frame: any, frameIndex: number) => (
<div key={frameIndex} style={{
marginBottom: '4px',
paddingBottom: '4px',
borderBottom: frameIndex < Math.min(stackTrace.length, 10) - 1 ? '1px solid #e9ecef' : 'none'
}}>
<div style={{ color: '#007bff', fontWeight: 'bold' }}>
{frame.functionName || '(anonymous)'}
</div>
<div style={{ color: '#6c757d' }}>
{frame.url ? truncateText(frame.url, 80) : 'unknown'}
{frame.lineNumber && `:${frame.lineNumber}`}
{frame.columnNumber && `:${frame.columnNumber}`}
</div>
</div>
))}
{stackTrace.length > 10 && (
<div style={{ color: '#6c757d', fontStyle: 'italic', marginTop: '8px' }}>
... and {stackTrace.length - 10} more frames
</div>
)}
</div>
</div>
)}
</div>
</td>
</tr>
)}
</>
)
})}
</tbody>
</table>
</div>
{paginatedEvents.length === 0 && (
<div style={{
textAlign: 'center',
padding: '40px',
color: '#6c757d',
fontSize: '16px'
}}>
No events found matching the current filters
</div>
)}
<style>{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
tr:hover {
background: #f8f9fa !important;
}
`}</style>
</div>
)
}
function getPhaseColor(phase: TraceEventPhase): string {
const colors: Record<string, string> = {
'M': '#6c757d', // Metadata - gray
'X': '#007bff', // Complete - blue
'I': '#28a745', // Instant - green
'B': '#ffc107', // Begin - yellow
'E': '#fd7e14', // End - orange
'D': '#6f42c1', // Deprecated - purple
'b': '#20c997', // Nestable start - teal
'e': '#e83e8c', // Nestable end - pink
'n': '#17a2b8', // Nestable instant - cyan
'S': '#dc3545', // Async start - red
'T': '#ffc107', // Async instant - warning
'F': '#fd7e14', // Async end - orange
'P': '#6f42c1', // Sample - purple
'C': '#20c997', // Counter - teal
'R': '#e83e8c' // Mark - pink
}
return colors[phase] || '#6c757d'
}