import { useState, useMemo } from 'react' import { useDatabaseTraceData } from '../hooks/useDatabaseTraceData' import type { TraceEventPhase } from '../../types/trace' interface ExtendedTraceEvent { args: Record cat: string name: string ph: TraceEventPhase pid: number tid: number ts: number tts?: number dur?: number tdur?: number } const PHASE_DESCRIPTIONS: Record = { '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('all') const [currentPage, setCurrentPage] = useState(1) const [searchTerm, setSearchTerm] = useState('') const [expandedRows, setExpandedRows] = useState>(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 (
Loading trace events...
) } if (error) { return (

Error Loading Trace Data

{error}

) } if (!traceData || !stats) { return
No trace data available
} const phaseOptions = Object.keys(stats.eventsByPhase).sort() return (

Phase Event Viewer

{/* Controls */}
{/* Phase Filter */}
{/* Search */}
{ setSearchTerm(e.target.value) setCurrentPage(1) }} style={{ padding: '8px 12px', border: '1px solid #ced4da', borderRadius: '4px', fontSize: '14px', minWidth: '300px' }} />
{/* Results count */}
{filteredEvents.length.toLocaleString()} events found
{/* Pagination Controls */} {totalPages > 1 && (
Page {currentPage} of {totalPages}
)} {/* Events Table */}
{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 ( <> toggleRowExpansion(eventKey)} > {/* Expanded Row Details */} {isExpanded && ( )} ) })}
Expand Phase Name Category Duration Thread Time URL PID TID
{isExpanded ? '−' : '+'} {event.ph} {event.name} {event.cat} {formatDuration((event as ExtendedTraceEvent).dur)} {formatThreadTime((event as ExtendedTraceEvent).tts)} {url ? ( {truncateText(url, 40)} ) : '-'} {event.pid} {event.tid}
{/* Timing Information */}

Timing

Timestamp: {formatTimestamp(event.ts)}
Duration: {formatDuration((event as ExtendedTraceEvent).dur)}
Thread Duration: {formatDuration((event as ExtendedTraceEvent).tdur)}
Thread Time: {formatThreadTime((event as ExtendedTraceEvent).tts)}
{sampleTraceId &&
Sample Trace ID: {sampleTraceId}
}
{/* Script/Context Information */} {(scriptInfo.contextId || scriptInfo.scriptId) && (

Script Context

{scriptInfo.contextId &&
Execution Context: {scriptInfo.contextId}
} {scriptInfo.scriptId &&
Script ID: {scriptInfo.scriptId}
}
)} {/* Layout Information */} {(layoutInfo.dirtyObjects || layoutInfo.totalObjects || layoutInfo.elementCount) && (

Layout Metrics

{layoutInfo.dirtyObjects &&
Dirty Objects: {layoutInfo.dirtyObjects}
} {layoutInfo.totalObjects &&
Total Objects: {layoutInfo.totalObjects}
} {layoutInfo.elementCount &&
Element Count: {layoutInfo.elementCount}
}
)} {/* Stack Trace */} {stackTrace && stackTrace.length > 0 && (

Stack Trace ({stackTrace.length} frames)

{stackTrace.slice(0, 10).map((frame: any, frameIndex: number) => (
{frame.functionName || '(anonymous)'}
{frame.url ? truncateText(frame.url, 80) : 'unknown'} {frame.lineNumber && `:${frame.lineNumber}`} {frame.columnNumber && `:${frame.columnNumber}`}
))} {stackTrace.length > 10 && (
... and {stackTrace.length - 10} more frames
)}
)}
{paginatedEvents.length === 0 && (
No events found matching the current filters
)}
) } function getPhaseColor(phase: TraceEventPhase): string { const colors: Record = { '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' }