diff --git a/package-lock.json b/package-lock.json
index 4afb441..5645da5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,7 +10,8 @@
"dependencies": {
"babylonjs": "^8.21.1",
"react": "^19.1.0",
- "react-dom": "^19.1.0"
+ "react-dom": "^19.1.0",
+ "ssim.js": "^3.5.0"
},
"devDependencies": {
"@eslint/js": "^9.30.1",
@@ -3041,6 +3042,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/ssim.js": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/ssim.js/-/ssim.js-3.5.0.tgz",
+ "integrity": "sha512-Aj6Jl2z6oDmgYFFbQqK7fght19bXdOxY7Tj03nF+03M9gCBAjeIiO8/PlEGMfKDwYpw4q6iBqVq2YuREorGg/g==",
+ "license": "MIT"
+ },
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
diff --git a/package.json b/package.json
index 3542a4b..45fe6e7 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,8 @@
"dependencies": {
"babylonjs": "^8.21.1",
"react": "^19.1.0",
- "react-dom": "^19.1.0"
+ "react-dom": "^19.1.0",
+ "ssim.js": "^3.5.0"
},
"devDependencies": {
"@eslint/js": "^9.30.1",
diff --git a/src/App.tsx b/src/App.tsx
index 7cb3e14..e942589 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,6 +1,5 @@
import { useState, useEffect } from 'react'
import './App.css'
-import BabylonViewer from './BabylonViewer'
import TraceViewer from './components/TraceViewer'
import PhaseViewer from './components/PhaseViewer'
import HTTPRequestViewer from './components/HTTPRequestViewer'
@@ -8,7 +7,7 @@ import TraceUpload from './components/TraceUpload'
import TraceSelector from './components/TraceSelector'
import { traceDatabase } from './utils/traceDatabase'
-type AppView = '3d' | 'trace' | 'phases' | 'http'
+type AppView = 'trace' | 'phases' | 'http'
type AppMode = 'selector' | 'upload' | 'analysis'
function App() {
@@ -123,20 +122,6 @@ function App() {
- {currentView === '3d' && (
-
-
-
- )}
-
{currentView === 'trace' && (
)}
diff --git a/src/BabylonTimelineViewer.tsx b/src/BabylonTimelineViewer.tsx
new file mode 100644
index 0000000..0092cdd
--- /dev/null
+++ b/src/BabylonTimelineViewer.tsx
@@ -0,0 +1,458 @@
+import { useEffect, useRef } from 'react'
+import {
+ Engine,
+ Scene,
+ UniversalCamera,
+ Vector3,
+ HemisphericLight,
+ MeshBuilder,
+ StandardMaterial,
+ Color3,
+ DynamicTexture
+} from 'babylonjs'
+
+// Helper function to create timeline labels
+function createTimelineLabel(
+ scene: Scene,
+ labelId: string,
+ position: Vector3,
+ actualTimeOffset: number
+): void {
+ // Calculate time conversions
+ const actualTimeMs = actualTimeOffset / 1000 // Convert to milliseconds
+ const actualTimeSeconds = actualTimeOffset / 1000000 // Convert to seconds
+
+ // Format time label based on magnitude (assuming microseconds input)
+ let timeLabelText = ''
+ if (actualTimeSeconds >= 1) {
+ // If >= 1 second, show in seconds
+ timeLabelText = `${actualTimeSeconds.toFixed(1)}s`
+ } else if (actualTimeMs >= 1) {
+ // If >= 1 millisecond, show in milliseconds
+ timeLabelText = `${Math.round(actualTimeMs)}ms`
+ } else {
+ // If < 1 millisecond, show in microseconds
+ timeLabelText = `${Math.round(actualTimeOffset)}μs`
+ }
+
+ // Create label texture
+ const labelTexture = new DynamicTexture(`timeLabel_${labelId}`, { width: 80, height: 32 }, scene)
+ labelTexture.hasAlpha = true
+ labelTexture.drawText(timeLabelText, null, null, '12px Arial', 'white', 'rgba(0,0,0,0.5)', true)
+
+ // Create label plane
+ const timeLabelPlane = MeshBuilder.CreatePlane(`timeLabelPlane_${labelId}`, { size: 0.5 }, scene)
+ timeLabelPlane.position = position
+
+ // Create and apply material
+ const timeLabelMaterial = new StandardMaterial(`timeLabelMaterial_${labelId}`, scene)
+ timeLabelMaterial.diffuseTexture = labelTexture
+ timeLabelMaterial.hasAlpha = true
+ timeLabelMaterial.backFaceCulling = false
+ timeLabelPlane.material = timeLabelMaterial
+}
+
+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 BabylonTimelineViewerProps {
+ width?: number
+ height?: number
+ httpRequests?: HTTPRequest[]
+}
+
+export default function BabylonTimelineViewer({ width = 800, height = 600, httpRequests = [] }: BabylonTimelineViewerProps) {
+ const canvasRef = useRef(null)
+ const engineRef = useRef(null)
+ const sceneRef = useRef(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
+
+ // Create universal camera for enhanced keyboard and mouse navigation
+ const camera = new UniversalCamera('camera1', new Vector3(0, 5, -10), scene)
+ camera.attachControl(canvas, true)
+
+ // Set camera target to look at the origin
+ camera.setTarget(new Vector3(0, 0, 0))
+
+ // Configure camera movement speed and sensitivity
+ camera.speed = 0.5
+ camera.angularSensibility = 2000
+
+ // Enable WASD keys explicitly
+ camera.keysUp = [87] // W key
+ camera.keysDown = [83] // S key
+ camera.keysLeft = [65] // A key
+ camera.keysRight = [68] // D key
+
+ scene.activeCamera = camera
+
+ const light = new HemisphericLight('light', new Vector3(0, 1, 0), scene)
+ light.intensity = 0.7
+
+ // Create ground plane oriented along the Z-axis timeline
+ const ground = MeshBuilder.CreateGround('ground', { width: 20, height: 12 }, scene)
+ ground.rotation.x = -Math.PI / 2 // Rotate to lie along Z-axis
+ ground.position = new Vector3(0, 0, 5) // Center at middle of timeline
+ const groundMaterial = new StandardMaterial('groundMaterial', scene)
+ groundMaterial.diffuseColor = new Color3(0.2, 0.2, 0.2)
+ groundMaterial.alpha = 0.3
+ ground.material = groundMaterial
+
+ // Create timeline start marker at Z = 0
+ const startMarker = MeshBuilder.CreateSphere('timelineStart', { diameter: 0.3 }, scene)
+ startMarker.position = new Vector3(0, 0, 0)
+ const startMaterial = new StandardMaterial('startMaterial', scene)
+ startMaterial.diffuseColor = new Color3(0.2, 0.8, 0.2) // Green for start
+ startMaterial.specularColor = new Color3(0.5, 1, 0.5)
+ startMarker.material = startMaterial
+
+ // Create timeline end marker at Z = 10
+ const endMarker = MeshBuilder.CreateSphere('timelineEnd', { diameter: 0.3 }, scene)
+ endMarker.position = new Vector3(0, 0, 10)
+ const endMaterial = new StandardMaterial('endMaterial', scene)
+ endMaterial.diffuseColor = new Color3(0.8, 0.2, 0.2) // Red for end
+ endMaterial.specularColor = new Color3(1, 0.5, 0.5)
+ endMarker.material = endMaterial
+
+
+ // Visualize HTTP requests in timeline swimlanes by hostname
+ if (httpRequests && httpRequests.length > 0) {
+ // Group requests by hostname
+ const requestsByHostname = new Map()
+ 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 Z-axis timeline normalization
+ const startTimes = httpRequests.map(req => req.timing.startOffset || 0)
+ const endTimes = httpRequests.map(req => (req.timing.startOffset || 0) + (req.timing.duration || 0))
+ const minStartTime = Math.min(...startTimes)
+ const maxEndTime = Math.max(...endTimes)
+ const totalTimeRange = maxEndTime - minStartTime
+
+ // Debug: Check the actual timing values (assuming microseconds)
+ console.log(`Timeline debug: minStart=${minStartTime}μs, maxEnd=${maxEndTime}μs, totalRange=${totalTimeRange}μs`)
+ console.log(`Timeline debug: totalRange in ms=${totalTimeRange/1000}ms, in seconds=${totalTimeRange/1000000}s`)
+ console.log(`Sample request timing:`, httpRequests[0]?.timing)
+
+ // 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())
+
+ // Sort hostnames by their earliest request start time
+ const hostnamesWithStartTimes = hostnames.map(hostname => {
+ const requests = requestsByHostname.get(hostname)!
+ const earliestStartTime = Math.min(...requests.map(req => req.timing.startOffset || 0))
+ return { hostname, earliestStartTime }
+ })
+ hostnamesWithStartTimes.sort((a, b) => a.earliestStartTime - b.earliestStartTime)
+
+ const hostCount = hostnames.length
+ const minZ = 0 // Timeline start at Z = 0
+ const maxZ = 20 // Timeline end at Z = 10
+ const minHeight = 0.1 // Minimum box height (0.1 meters)
+ const maxHeight = 2 // Maximum box height (5 meters)
+ const swimlaneSpacing = 1.5 // Spacing between swimlanes on X-axis
+
+ // Calculate the full width needed to span all swimlanes
+ const totalWidth = (hostCount - 1) * swimlaneSpacing + 4 // Add padding on both sides
+
+ // Create timeline grid lines at regular intervals across the normalized 0-10 timeline
+ const gridLineCount = 10 // Create 10 grid lines across the timeline
+ for (let i = 0; i <= gridLineCount; i++) {
+ const zPosition = minZ + (i / gridLineCount) * (maxZ - minZ) // Evenly spaced from 0 to 10
+
+ // Calculate the actual time this position represents
+ const actualTimeOffset = (i / gridLineCount) * totalTimeRange // This is in microseconds
+ const actualTimeMs = actualTimeOffset / 1000 // Convert to milliseconds
+ const actualTimeSeconds = actualTimeOffset / 1000000 // Convert to seconds
+
+ // Debug: Let's see what the actual values are
+ console.log(`Grid line ${i}: offset=${actualTimeOffset}μs, ms=${actualTimeMs}ms, seconds=${actualTimeSeconds}s, totalRange=${totalTimeRange}μs`)
+
+ // Create grid line spanning the full width of all content
+ const linePoints = [
+ new Vector3(-totalWidth / 2, 0, zPosition),
+ new Vector3(totalWidth / 2, 0, zPosition)
+ ]
+
+ const gridLine = MeshBuilder.CreateLines(`gridLine_${i}`, { points: linePoints }, scene)
+ const gridMaterial = new StandardMaterial(`gridMaterial_${i}`, scene)
+ gridMaterial.diffuseColor = new Color3(0.5, 0.5, 0.5)
+ gridMaterial.alpha = 0.3
+
+ // Add timeline label for each grid line
+ createTimelineLabel(scene, i.toString(), new Vector3(-0.5, -0.5, zPosition), actualTimeOffset)
+ }
+
+ hostnamesWithStartTimes.forEach(({ hostname }, sortedIndex) => {
+ // Calculate X position alternating left/right from origin, with distance increasing by start time order
+ // Index 0 (earliest) -> X = 0, Index 1 -> X = -2, Index 2 -> X = 2, Index 3 -> X = -4, Index 4 -> X = 4, etc.
+ let xPosition = 0
+ if (sortedIndex > 0) {
+ const distance = Math.ceil(sortedIndex / 2) * swimlaneSpacing
+ xPosition = (sortedIndex % 2 === 1) ? -distance : distance
+ }
+
+ // Create hostname label at the start of each swimlane
+ const labelTexture = new DynamicTexture(`hostLabel_${sortedIndex}`, { width: 256, height: 64 }, scene)
+ labelTexture.hasAlpha = true
+ labelTexture.drawText(
+ hostname,
+ null, null,
+ '16px Arial',
+ 'white',
+ 'rgba(0,0,0,0.7)',
+ true
+ )
+
+ const hostLabel = MeshBuilder.CreatePlane(`hostLabelPlane_${sortedIndex}`, { size: 1.5 }, scene)
+ hostLabel.position = new Vector3(xPosition, -1, -1) // Position at Y = -1, Z = -1 (below ground, before timeline start)
+
+ const labelMaterial = new StandardMaterial(`hostLabelMaterial_${sortedIndex}`, scene)
+ labelMaterial.diffuseTexture = labelTexture
+ labelMaterial.hasAlpha = 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<{
+ startZ: number
+ endZ: number
+ height: number
+ yPosition: number
+ }> = []
+
+ // Create boxes for each request from this hostname
+ hostnameRequests.forEach((request, requestIndex) => {
+ // Calculate Z positions based on timeline (start time to end time)
+ const startTime = request.timing.startOffset || 0
+ const duration = request.timing.duration || 1000 // Default 1ms if no duration
+ const endTime = startTime + duration
+
+ // Normalize to Z-axis range (0 to 10)
+ const normalizedStartTime = totalTimeRange > 0 ? (startTime - minStartTime) / totalTimeRange : 0
+ const normalizedEndTime = totalTimeRange > 0 ? (endTime - minStartTime) / totalTimeRange : 0
+
+ const startZ = minZ + (normalizedStartTime * (maxZ - minZ))
+ const endZ = minZ + (normalizedEndTime * (maxZ - minZ))
+
+ // Calculate box depth (duration) and center Z position
+ const boxDepth = Math.max(0.05, endZ - startZ) // Minimum depth of 0.05m
+ const centerZ = startZ + (boxDepth / 2)
+
+ // 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 in this swimlane
+ let stackHeight = 0
+ for (const placedBox of placedBoxes) {
+ // Check if this request overlaps with a placed box on the timeline (Z-axis)
+ if (startZ < placedBox.endZ && endZ > placedBox.startZ) {
+ // 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)
+
+ // Calculate server latency and network time portions
+ const serverLatency = request.timing.serverLatency || 0
+ const networkTime = duration - serverLatency
+
+ // Calculate proportional depths for server vs network time
+ const serverLatencyRatio = serverLatency > 0 ? serverLatency / duration : 0
+ const networkTimeRatio = networkTime > 0 ? networkTime / duration : 1
+
+ const serverDepth = boxDepth * serverLatencyRatio
+ const networkDepth = boxDepth * networkTimeRatio
+
+ // Calculate color based on vertical position (same gradient for both boxes)
+ const verticalColorIntensity = Math.min(1.0, Math.max(0.3, (stackHeight + boxHeight) / 8.0))
+
+ // Create server time box (gray) - positioned at the start of the request
+ if (serverDepth > 0.01) { // Only create if server time is significant
+ const serverCenterZ = startZ + (serverDepth / 2)
+ const serverBox = MeshBuilder.CreateBox(`serverBox_${sortedIndex}_${requestIndex}`, {
+ width: 0.2,
+ height: boxHeight,
+ depth: serverDepth
+ }, scene)
+
+ serverBox.position = new Vector3(xPosition, yPosition, serverCenterZ)
+
+ // Gray material with gradient and 50% opacity
+ const serverMaterial = new StandardMaterial(`serverMaterial_${sortedIndex}_${requestIndex}`, scene)
+ serverMaterial.diffuseColor = new Color3(verticalColorIntensity * 0.5, verticalColorIntensity * 0.5, verticalColorIntensity * 0.5)
+ serverMaterial.alpha = 0.5 // 50% opacity for server time boxes
+
+ if (request.fromCache) {
+ serverMaterial.alpha = 0.35 // Reduce further for cached requests (0.5 * 0.7)
+ }
+
+ serverBox.material = serverMaterial
+ }
+
+ // Create network time box (blue) - positioned after server time
+ const networkStartZ = startZ + serverDepth
+ const networkCenterZ = networkStartZ + (networkDepth / 2)
+
+ const networkBox = MeshBuilder.CreateBox(`networkBox_${sortedIndex}_${requestIndex}`, {
+ width: 0.2,
+ height: boxHeight,
+ depth: networkDepth
+ }, scene)
+
+ networkBox.position = new Vector3(xPosition, yPosition, networkCenterZ)
+
+ // Add this box to the placed boxes list for future overlap checks
+ placedBoxes.push({
+ startZ,
+ endZ,
+ height: boxHeight,
+ yPosition: stackHeight + boxHeight
+ })
+
+ // Blue material with gradient and status code coloring
+ const networkMaterial = new StandardMaterial(`networkMaterial_${sortedIndex}_${requestIndex}`, scene)
+ if (request.statusCode) {
+ if (request.statusCode >= 200 && request.statusCode < 300) {
+ // Blue with gradient for success
+ networkMaterial.diffuseColor = new Color3(0, 0, verticalColorIntensity)
+ } else if (request.statusCode >= 300 && request.statusCode < 400) {
+ networkMaterial.diffuseColor = new Color3(1, 1, 0) // Yellow for redirects
+ } else if (request.statusCode >= 400 && request.statusCode < 500) {
+ networkMaterial.diffuseColor = new Color3(1, 0.5, 0) // Orange for client errors
+ } else if (request.statusCode >= 500) {
+ networkMaterial.diffuseColor = new Color3(1, 0, 0) // Red for server errors
+ }
+ } else {
+ networkMaterial.diffuseColor = new Color3(0, 0, 0.5) // Dark blue for unknown
+ }
+
+ // Add transparency for cached requests
+ if (request.fromCache) {
+ networkMaterial.alpha = 0.7
+ }
+
+ networkBox.material = networkMaterial
+
+ // Add yellow queue time line if queue time exists
+ if (request.timing.queueTime && request.timing.queueTime > 0) {
+ // Calculate queue start time (when request was first queued - before the actual request start)
+ const queueStartTime = startTime - request.timing.queueTime
+ const queueEndTime = startTime // Queue ends when actual request processing starts
+
+ // Normalize queue start and end times to Z-axis
+ const normalizedQueueStartTime = totalTimeRange > 0 ? Math.max(0, (queueStartTime - minStartTime) / totalTimeRange) : 0
+ const normalizedQueueEndTime = totalTimeRange > 0 ? (queueEndTime - minStartTime) / totalTimeRange : 0
+
+ const queueStartZ = minZ + (normalizedQueueStartTime * (maxZ - minZ))
+ const queueEndZ = minZ + (normalizedQueueEndTime * (maxZ - minZ))
+
+ // Calculate queue line depth (from queue start to queue end)
+ const queueLineDepth = Math.max(0.01, queueEndZ - queueStartZ) // Minimum depth of 0.01m
+ const queueLineCenterZ = queueStartZ + (queueLineDepth / 2)
+
+ // Create yellow queue time line positioned slightly above the request box
+ const queueLine = MeshBuilder.CreateBox(`queueLine_${sortedIndex}_${requestIndex}`, {
+ width: 0.05, // Thin line width
+ height: 0.05, // Thin line height
+ depth: queueLineDepth
+ }, scene)
+
+ // Position queue line at the same vertical center as the request box
+ queueLine.position = new Vector3(xPosition, yPosition, queueLineCenterZ)
+
+ // Create yellow material for queue time with 25% opacity
+ const queueMaterial = new StandardMaterial(`queueMaterial_${sortedIndex}_${requestIndex}`, scene)
+ queueMaterial.diffuseColor = new Color3(1, 1, 0) // Yellow color
+ queueMaterial.emissiveColor = new Color3(0.2, 0.2, 0) // Slight glow
+ queueMaterial.alpha = 0.25 // 25% opacity for queue time boxes
+ queueLine.material = queueMaterial
+ }
+ })
+
+ // Create a swimlane line from timeline start to end for visual reference
+ const swimlaneLinePoints = [new Vector3(xPosition, 0, minZ), new Vector3(xPosition, 0, maxZ)]
+ const swimlaneLine = MeshBuilder.CreateLines(`swimlaneLine_${sortedIndex}`, { points: swimlaneLinePoints }, scene)
+ const lineMaterial = new StandardMaterial(`swimlaneLineMaterial_${sortedIndex}`, scene)
+ lineMaterial.diffuseColor = new Color3(0.4, 0.4, 0.4)
+ lineMaterial.alpha = 0.3
+ })
+ }
+
+ engine.runRenderLoop(() => {
+ scene.render()
+ })
+
+ const handleResize = () => {
+ engine.resize()
+ }
+
+ window.addEventListener('resize', handleResize)
+
+ return () => {
+ window.removeEventListener('resize', handleResize)
+ engine.dispose()
+ }
+ }, [httpRequests])
+
+ return (
+
+ )
+}
\ No newline at end of file
diff --git a/src/BabylonViewer.tsx b/src/BabylonViewer.tsx
index fb90938..7c35d62 100644
--- a/src/BabylonViewer.tsx
+++ b/src/BabylonViewer.tsx
@@ -7,15 +7,42 @@ import {
HemisphericLight,
MeshBuilder,
StandardMaterial,
- Color3
+ Color3,
+ Mesh,
+ 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 }: BabylonViewerProps) {
+export default function BabylonViewer({ width = 800, height = 600, httpRequests = [] }: BabylonViewerProps) {
const canvasRef = useRef(null)
const engineRef = useRef(null)
const sceneRef = useRef(null)
@@ -30,26 +57,217 @@ export default function BabylonViewer({ width = 800, height = 600 }: BabylonView
const scene = new Scene(engine)
sceneRef.current = scene
- const camera = new ArcRotateCamera('camera1', -Math.PI / 2, Math.PI / 2.5, 10, Vector3.Zero(), 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
- const sphere = MeshBuilder.CreateSphere('sphere', { diameter: 2 }, scene)
- sphere.position.y = 1
-
- const ground = MeshBuilder.CreateGround('ground', { width: 6, height: 6 }, scene)
-
- const sphereMaterial = new StandardMaterial('sphereMaterial', scene)
- sphereMaterial.diffuseColor = new Color3(1, 0, 1)
- sphere.material = sphereMaterial
-
+ // 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.5, 0.5, 0.5)
+ 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()
+ 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
+
+ // Find min and max duration values for depth normalization
+ const durations = httpRequests
+ .map(req => req.timing.duration || 0)
+ .filter(duration => duration > 0)
+ const minDuration = durations.length > 0 ? Math.min(...durations) : 1000
+ const maxDuration = durations.length > 0 ? Math.max(...durations) : 1000
+ const durationRange = maxDuration - minDuration
+
+ 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)
+ const maxDepth = 2 // Maximum box depth (2 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)
+ labelTexture.hasAlpha = true
+ 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.hasAlpha = 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)]
+ const line = 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()
})
@@ -64,7 +282,7 @@ export default function BabylonViewer({ width = 800, height = 600 }: BabylonView
window.removeEventListener('resize', handleResize)
engine.dispose()
}
- }, [])
+ }, [httpRequests])
return (