🚀 Features implemented: - IndexedDB local database for trace storage and management - Drag & drop trace file upload with validation - HTTP Request viewer with advanced filtering and analysis - CDN provider detection (Cloudflare, Akamai, CloudFront, etc.) - Queue time analysis with bottleneck detection - Visual highlighting for file sizes and request durations - Priority-based request analysis - Phase event viewer with detailed trace exploration - Filmstrip screenshot integration (with debugging) - 3D Babylon.js viewer component 📊 Analysis capabilities: - HTTP/1.1 vs HTTP/2 performance comparison - CDN edge vs origin detection - Connection limit bottleneck identification - Priority queue analysis - Visual correlation with network requests - Performance bottleneck identification 🛠️ Technical stack: - React 19.1.0 + TypeScript 5.8.3 - Vite build system - IndexedDB for local storage - Babylon.js 8+ for 3D visualization - Chrome DevTools trace format support 🎨 User experience: - Clean, professional interface design - Color-coded performance indicators - Expandable detailed views - Search and filtering capabilities - Responsive grid layouts - Intuitive navigation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
165 lines
5.4 KiB
TypeScript
165 lines
5.4 KiB
TypeScript
import { getTraceStats } from '../utils/traceLoader'
|
|
|
|
interface TraceStatsProps {
|
|
stats: ReturnType<typeof getTraceStats>
|
|
}
|
|
|
|
export default function TraceStats({ stats }: TraceStatsProps) {
|
|
const formatNumber = (num: number) => {
|
|
return new Intl.NumberFormat().format(num)
|
|
}
|
|
|
|
const formatDuration = (microseconds: number) => {
|
|
const milliseconds = microseconds / 1000
|
|
if (milliseconds < 1000) {
|
|
return `${milliseconds.toFixed(2)}ms`
|
|
}
|
|
const seconds = milliseconds / 1000
|
|
return `${seconds.toFixed(2)}s`
|
|
}
|
|
|
|
const formatTimestamp = (timestamp: number) => {
|
|
return new Date(timestamp / 1000).toLocaleString()
|
|
}
|
|
|
|
const getPhaseDescription = (phase: string) => {
|
|
const 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'
|
|
}
|
|
return descriptions[phase] || `Unknown (${phase})`
|
|
}
|
|
|
|
return (
|
|
<div style={{
|
|
padding: '20px',
|
|
fontFamily: 'system-ui, sans-serif',
|
|
maxWidth: '800px',
|
|
margin: '0 auto'
|
|
}}>
|
|
<h2 style={{ marginBottom: '20px', color: '#333' }}>Trace Statistics</h2>
|
|
|
|
<div style={{
|
|
display: 'grid',
|
|
gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
|
|
gap: '20px',
|
|
marginBottom: '30px'
|
|
}}>
|
|
{/* Overview Section */}
|
|
<div style={{
|
|
background: '#f8f9fa',
|
|
padding: '15px',
|
|
borderRadius: '8px',
|
|
border: '1px solid #e9ecef'
|
|
}}>
|
|
<h3 style={{ marginTop: '0', color: '#495057' }}>Overview</h3>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
|
<div><strong>Total Events:</strong> {formatNumber(stats.totalEvents)}</div>
|
|
<div><strong>Processes:</strong> {stats.processCount}</div>
|
|
<div><strong>Threads:</strong> {stats.threadCount}</div>
|
|
<div><strong>Duration:</strong> {formatDuration(stats.timeRange.duration)}</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Metadata Section */}
|
|
<div style={{
|
|
background: '#f8f9fa',
|
|
padding: '15px',
|
|
borderRadius: '8px',
|
|
border: '1px solid #e9ecef'
|
|
}}>
|
|
<h3 style={{ marginTop: '0', color: '#495057' }}>Metadata</h3>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
|
<div><strong>Source:</strong> {stats.metadata.source}</div>
|
|
<div><strong>Hardware Threads:</strong> {stats.metadata.hardwareConcurrency}</div>
|
|
<div><strong>Start Time:</strong> {new Date(stats.metadata.startTime).toLocaleString()}</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Time Range Section */}
|
|
<div style={{
|
|
background: '#f8f9fa',
|
|
padding: '15px',
|
|
borderRadius: '8px',
|
|
border: '1px solid #e9ecef'
|
|
}}>
|
|
<h3 style={{ marginTop: '0', color: '#495057' }}>Time Range</h3>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', fontSize: '12px' }}>
|
|
<div><strong>Start:</strong> {formatTimestamp(stats.timeRange.start)}</div>
|
|
<div><strong>End:</strong> {formatTimestamp(stats.timeRange.end)}</div>
|
|
<div><strong>Duration:</strong> {formatDuration(stats.timeRange.duration)}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Event Types Section */}
|
|
<div style={{
|
|
background: '#f8f9fa',
|
|
padding: '20px',
|
|
borderRadius: '8px',
|
|
border: '1px solid #e9ecef'
|
|
}}>
|
|
<h3 style={{ marginTop: '0', color: '#495057' }}>Events by Type</h3>
|
|
<div style={{
|
|
display: 'grid',
|
|
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
|
|
gap: '10px'
|
|
}}>
|
|
{Object.entries(stats.eventsByPhase)
|
|
.sort(([, a], [, b]) => b - a)
|
|
.map(([phase, count]) => (
|
|
<div key={phase} style={{
|
|
background: 'white',
|
|
padding: '10px',
|
|
borderRadius: '4px',
|
|
border: '1px solid #dee2e6',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: '4px'
|
|
}}>
|
|
<div style={{
|
|
fontWeight: 'bold',
|
|
color: '#495057',
|
|
fontSize: '14px'
|
|
}}>
|
|
{getPhaseDescription(phase)}
|
|
</div>
|
|
<div style={{
|
|
fontSize: '12px',
|
|
color: '#6c757d'
|
|
}}>
|
|
Phase: {phase}
|
|
</div>
|
|
<div style={{
|
|
fontSize: '16px',
|
|
fontWeight: 'bold',
|
|
color: '#007bff'
|
|
}}>
|
|
{formatNumber(count)}
|
|
</div>
|
|
<div style={{
|
|
fontSize: '11px',
|
|
color: '#6c757d'
|
|
}}>
|
|
{((count / stats.totalEvents) * 100).toFixed(1)}%
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
} |