perfViz/src/BabylonViewer.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

286 lines
11 KiB
TypeScript

import { useEffect, useRef } from 'react'
import {
Engine,
Scene,
ArcRotateCamera,
Vector3,
HemisphericLight,
MeshBuilder,
StandardMaterial,
Color3,
DynamicTexture
} from 'babylonjs'
interface HTTPRequest {
requestId: string
url: string
hostname: string
method: string
resourceType: string
priority: string
statusCode?: number
mimeType?: string
protocol?: string
timing: {
start: number
startOffset?: number
end?: number
duration?: number
queueTime?: number
serverLatency?: number
}
encodedDataLength?: number
contentLength?: number
fromCache: boolean
connectionReused: boolean
}
interface BabylonViewerProps {
width?: number
height?: number
httpRequests?: HTTPRequest[]
}
export default function BabylonViewer({ width = 800, height = 600, httpRequests = [] }: BabylonViewerProps) {
const canvasRef = useRef<HTMLCanvasElement>(null)
const engineRef = useRef<Engine | null>(null)
const sceneRef = useRef<Scene | null>(null)
useEffect(() => {
if (!canvasRef.current) return
const canvas = canvasRef.current
const engine = new Engine(canvas, true)
engineRef.current = engine
const scene = new Scene(engine)
sceneRef.current = scene
const camera = new ArcRotateCamera('camera1', -Math.PI / 2, Math.PI / 2.5, 25, Vector3.Zero(), scene)
camera.attachControl(canvas, true)
scene.activeCamera = camera
const light = new HemisphericLight('light', new Vector3(0, 1, 0), scene)
light.intensity = 0.7
// Create ground plane for reference
const ground = MeshBuilder.CreateGround('ground', { width: 50, height: 30 }, scene)
const groundMaterial = new StandardMaterial('groundMaterial', scene)
groundMaterial.diffuseColor = new Color3(0.2, 0.2, 0.2)
groundMaterial.alpha = 0.3
ground.material = groundMaterial
// Create central sphere at origin
const centralSphere = MeshBuilder.CreateSphere('centralSphere', { diameter: 0.5 }, scene)
centralSphere.position = Vector3.Zero()
const sphereMaterial = new StandardMaterial('sphereMaterial', scene)
sphereMaterial.diffuseColor = new Color3(0.3, 0.3, 0.8) // Blue sphere
sphereMaterial.specularColor = new Color3(0.5, 0.5, 1)
centralSphere.material = sphereMaterial
// Visualize HTTP requests as 3D objects arranged radially by hostname
if (httpRequests && httpRequests.length > 0) {
// Group requests by hostname
const requestsByHostname = new Map<string, HTTPRequest[]>()
httpRequests.forEach(request => {
if (!requestsByHostname.has(request.hostname)) {
requestsByHostname.set(request.hostname, [])
}
requestsByHostname.get(request.hostname)!.push(request)
})
// Find min and max start times for normalization
const startTimes = httpRequests.map(req => req.timing.startOffset || 0)
const minStartTime = Math.min(...startTimes)
const maxStartTime = Math.max(...startTimes)
const timeRange = maxStartTime - minStartTime
// Find min and max content-length values for height normalization
const contentLengths = httpRequests
.map(req => req.contentLength || 0)
.filter(size => size > 0) // Only consider requests with content-length
const minContentLength = contentLengths.length > 0 ? Math.min(...contentLengths) : 1
const maxContentLength = contentLengths.length > 0 ? Math.max(...contentLengths) : 1
const contentLengthRange = maxContentLength - minContentLength
const hostnames = Array.from(requestsByHostname.keys())
const hostCount = hostnames.length
const labelRadius = 12 // Distance from center for hostname labels
const minDistance = 1 // Minimum distance from center (1 meter)
const maxDistance = 10 // Maximum distance from center (10 meters)
const minHeight = 0.1 // Minimum box height (0.1 meters)
const maxHeight = 5 // Maximum box height (5 meters)
const minDepth = 0.05 // Minimum box depth (0.05 meters)
hostnames.forEach((hostname, hostIndex) => {
// Calculate radial position for this hostname
const angle = (hostIndex / hostCount) * 2 * Math.PI
const labelX = labelRadius * Math.cos(angle)
const labelZ = labelRadius * Math.sin(angle)
// Create hostname label that always faces camera
const labelTexture = new DynamicTexture(`hostLabel_${hostIndex}`, { width: 256, height: 64 }, scene)
// Note: hasAlpha property handled by BabylonJS
labelTexture.drawText(
hostname,
null, null,
'16px Arial',
'white',
'rgba(0,0,0,0.7)',
true
)
const hostLabel = MeshBuilder.CreatePlane(`hostLabelPlane_${hostIndex}`, { size: 1.5 }, scene)
hostLabel.position = new Vector3(labelX, 1.5, labelZ)
// Rotate label to face origin on vertical axis only, then add 180 degree rotation
const labelDirectionToOrigin = Vector3.Zero().subtract(hostLabel.position).normalize()
const labelYRotation = Math.atan2(labelDirectionToOrigin.x, labelDirectionToOrigin.z)
hostLabel.rotation.y = labelYRotation + Math.PI // Add 180 degrees (π radians)
const labelMaterial = new StandardMaterial(`hostLabelMaterial_${hostIndex}`, scene)
labelMaterial.diffuseTexture = labelTexture
labelMaterial.useAlphaFromDiffuseTexture = true
labelMaterial.backFaceCulling = false
hostLabel.material = labelMaterial
// Get requests for this hostname and sort by start time
const hostnameRequests = requestsByHostname.get(hostname)!
hostnameRequests.sort((a, b) => (a.timing.startOffset || 0) - (b.timing.startOffset || 0))
// Track placed boxes for this hostname to handle stacking
const placedBoxes: Array<{
startDistance: number
endDistance: number
height: number
yPosition: number
}> = []
// Create boxes for each request from this hostname
hostnameRequests.forEach((request, requestIndex) => {
// Calculate start and end distances from center based on timing
const startTime = request.timing.startOffset || 0
const duration = request.timing.duration || 1000 // Default 1ms if no duration
const endTime = startTime + duration
const normalizedStartTime = timeRange > 0 ? (startTime - minStartTime) / timeRange : 0
const normalizedEndTime = timeRange > 0 ? (endTime - minStartTime) / timeRange : 0
const startDistance = minDistance + (normalizedStartTime * (maxDistance - minDistance))
const endDistance = minDistance + (normalizedEndTime * (maxDistance - minDistance))
// Calculate box depth and center position
const boxDepth = Math.max(minDepth, endDistance - startDistance)
const centerDistance = startDistance + (boxDepth / 2)
// Position box at center distance along the hostname's angle
const boxX = centerDistance * Math.cos(angle)
const boxZ = centerDistance * Math.sin(angle)
// Calculate height based on content-length
const contentLength = request.contentLength || 0
let boxHeight = minHeight
if (contentLength > 0 && contentLengthRange > 0) {
const normalizedSize = (contentLength - minContentLength) / contentLengthRange
boxHeight = minHeight + (normalizedSize * (maxHeight - minHeight))
}
// Find the highest Y position for overlapping boxes
let stackHeight = 0
for (const placedBox of placedBoxes) {
// Check if this request overlaps with a placed box
if (startDistance < placedBox.endDistance && endDistance > placedBox.startDistance) {
// This box overlaps, so we need to stack it above
stackHeight = Math.max(stackHeight, placedBox.yPosition)
}
}
// Calculate Y position (center of the box) - stack height is the bottom of where this box should be
const yPosition = stackHeight + (boxHeight / 2)
// Create box with duration-based depth and content-length based height
const box = MeshBuilder.CreateBox(`request_${hostIndex}_${requestIndex}`, {
width: 0.1,
height: boxHeight,
depth: boxDepth
}, scene)
box.position = new Vector3(boxX, yPosition, boxZ)
// Add this box to the placed boxes list for future overlap checks
// yPosition here represents the top of this box for future stacking
placedBoxes.push({
startDistance,
endDistance,
height: boxHeight,
yPosition: stackHeight + boxHeight
})
// Rotate box to face the origin only on the vertical (Y) axis
const directionToOrigin = Vector3.Zero().subtract(box.position).normalize()
const yRotation = Math.atan2(directionToOrigin.x, directionToOrigin.z)
box.rotation.y = yRotation
// Calculate color based on vertical position for success requests
// Higher boxes are lighter (closer to white), lower boxes are darker
const verticalColorIntensity = Math.min(1.0, Math.max(0.3, (stackHeight + boxHeight) / 8.0))
// Color based on status code
const material = new StandardMaterial(`material_${hostIndex}_${requestIndex}`, scene)
if (request.statusCode) {
if (request.statusCode >= 200 && request.statusCode < 300) {
material.diffuseColor = new Color3(verticalColorIntensity, verticalColorIntensity, verticalColorIntensity)
} else if (request.statusCode >= 300 && request.statusCode < 400) {
material.diffuseColor = new Color3(1, 1, 0) // Yellow for redirects
} else if (request.statusCode >= 400 && request.statusCode < 500) {
material.diffuseColor = new Color3(1, 0.5, 0) // Orange for client errors
} else if (request.statusCode >= 500) {
material.diffuseColor = new Color3(1, 0, 0) // Red for server errors
}
} else {
material.diffuseColor = new Color3(0.5, 0.5, 0.5) // Gray for unknown
}
// Add transparency for cached requests
if (request.fromCache) {
material.alpha = 0.7
}
box.material = material
})
// Create a line from center to hostname label position for visual connection
const linePoints = [Vector3.Zero(), new Vector3(labelX * 0.8, 0, labelZ * 0.8)]
MeshBuilder.CreateLines(`connectionLine_${hostIndex}`, { points: linePoints }, scene)
const lineMaterial = new StandardMaterial(`lineMaterial_${hostIndex}`, scene)
lineMaterial.diffuseColor = new Color3(0.4, 0.4, 0.4)
lineMaterial.alpha = 0.5
})
}
engine.runRenderLoop(() => {
scene.render()
})
const handleResize = () => {
engine.resize()
}
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
engine.dispose()
}
}, [httpRequests])
return (
<canvas
ref={canvasRef}
width={width}
height={height}
style={{ width: '100%', height: '100%', display: 'block' }}
/>
)
}