infrastructure/load-balancing-for-inference/components/Play.tsx (485 lines of code) (raw):

"use client"; import { generatePlayerName } from "@/utils/name-generator"; import { Line } from "@react-three/drei"; import { Canvas, useLoader } from "@react-three/fiber"; import localFont from 'next/font/local'; import { memo, useEffect, useRef, useState } from "react"; import { TextureLoader } from "three"; import GpuGroup from "./GpuGroup"; const MessagePath = memo(Line); import Player1 from "./Player1"; import Player2 from "./Player2"; import CountdownOverlay from "./CountdownOverlay"; import GpuGroupRight from "./GpuGroupRight"; import ResultsOverlay from "./ResultsOverlay"; // Load Jersey15 font const jersey15 = localFont({ src: '../public/fonts/Jersey15-Regular.ttf', variable: '--font-jersey15' }); const totalGameTime = 60; const player1VmXPositions = [-5.0, -3.3, -2.0, -0.6]; const player2VmXPositions = player1VmXPositions .map((xPosition) => -1 * xPosition) .reverse(); const vmYPosition = -1.7; const vmYPositionTop = vmYPosition + 0.5; const vmYPositionBottom = vmYPosition - 0.6; const playerEndPositions: [number, number, number][] = player1VmXPositions.map( (xPosition) => [xPosition, vmYPositionBottom, 0] ); const loadBalancerXPosition = -2.9; const loadBalancerYPosition = vmYPositionTop + 2; const playerMidYPosition = loadBalancerYPosition; const playerOneLoadBalancerPosition: [number, number, number] = [ loadBalancerXPosition, loadBalancerYPosition, 0, ]; const lineOneStart: [number, number, number] = [ playerOneLoadBalancerPosition[0], playerOneLoadBalancerPosition[1] + 0.3, playerOneLoadBalancerPosition[2], ]; const playerTwoLoadBalancerPosition: [number, number, number] = [ -loadBalancerXPosition, loadBalancerYPosition, 0, ]; const lineTwoStart: [number, number, number] = [ playerTwoLoadBalancerPosition[0], playerTwoLoadBalancerPosition[1] + 0.3, playerTwoLoadBalancerPosition[2], ]; const startingBoxZ = -7; const colors = { utilization: "#FFFFFF", black: "#202124", red: "#EA4335", green: "#34A853", player1: "#FBBC04", player2: "#4285F4", }; const playerOneBlocks = Array.from(Array(50).keys()).map((index) => { const randomNumber = Math.random() - 0.5; const yPosition = 15 + index * 0.5 + randomNumber; const uuid = crypto.randomUUID(); return { xPosition: loadBalancerXPosition + randomNumber, // Fixed starting X position yPosition, uuid, }; }); const playerTwoBlocks = Array.from(Array(50).keys()).map((index) => { const randomNumber = Math.random() - 0.5; const xPosition = -loadBalancerXPosition + randomNumber; const yPosition = index / 2 + 7 + randomNumber; const uuid = crypto.randomUUID(); return { xPosition, yPosition, uuid }; }); type VmStatus = { cpu: number; memory: number; score: number; hostName: string; queue: number; tasksCompleted: number; tasksRegistered: number; utilization: number; atMaxCapacity: boolean; }; type VmStats = { statusArray: VmStatus[]; playerName: string; gameVmSelection: string; gameVmSelectionIndex: number; gameVmSelectionUpdates: number; playerOneScore: number; playerTwoScore: number; }; const defaultVmStatuses: VmStatus[] = Array.from(Array(8).keys()).map(() => ({ cpu: 0, memory: 11.5, hostName: "vm-default", score: 0, queue: 0, tasksCompleted: 0, tasksRegistered: 0, utilization: 0, atMaxCapacity: false, })); const defaultVmStats = { statusArray: defaultVmStatuses, playerName: "Default Player Name", gameVmSelection: "vm-default", gameVmSelectionIndex: 0, gameVmSelectionUpdates: 0, playerOneScore: 0, playerTwoScore: 0, }; type ResultBlock = { uid: string; startingPosition: [number, number, number]; }; type FailResultBlock = { uid: string; startingPosition: [number, number, number]; side: "LEFT" | "RIGHT"; }; const gpuXPositions = [...player1VmXPositions, ...player2VmXPositions]; // Define the base position of the robot on the rail const robotBasePosition: [number, number, number] = [3.0, 1.0, 0]; // Adjust as needed export default function Play() { // New GPU positions for each player. const player1GpuPositions = [-4.90, -3.69, -2.52, -1.31]; const player2GpuPositions = [1.19, 2.43, 3.64, 4.86]; const totalGameTime = 60; const [robotTargetX, setRobotTargetX] = useState(3.2); const [isGripping, setIsGripping] = useState(false); // TODO: Add pseudonyms for users const [playerOneEnd, setPlayerOneEnd] = useState<[number, number, number]>( playerEndPositions[0] ); const [timeElapsed, setTimeElapsed] = useState(-5); const [quarterSecondCounter, setQuarterSecondCounter] = useState(0); const [playerName, setPlayerName] = useState("You are the..."); const [failBlocks, setFailBlocks] = useState<FailResultBlock[]>([]); const [successBlocks, setSuccessBlocks] = useState<ResultBlock[]>([]); const [vmStats, setVmStats] = useState<VmStats>(defaultVmStats); const [playerTwoTotal, setTotalPlayerTwoTotal] = useState(0); const [gameStarted, setGameStarted] = useState(false); const [showResults, setShowResults] = useState(false); const [blocks, setBlocks] = useState< { uuid: string; gpuIndex: number; stackPosition: number; // NEW: Tracks stacking inside GPU }[] >([]); const userBoxTexture = useLoader(TextureLoader, "/assets/user-box.png"); const userFunnelTexture = useLoader(TextureLoader, "/assets/user-funnel.png"); const yellowDataBlockTexture = useLoader( TextureLoader, "/assets/yellow-datablock.png" ); // ✅ Define Robot Position Variables const robotGripperPosition: [number, number, number] = [3.0, -0.2, 0]; // Gripper at the end of Arm 2 // ✅ Load Textures const baseTexture = useLoader(TextureLoader, "/assets/base.png"); const arm1Texture = useLoader(TextureLoader, "/assets/arm1.png"); const arm2Texture = useLoader(TextureLoader, "/assets/arm2.png"); const railTexture = useLoader(TextureLoader, "/assets/rail.png"); const gripperTexture = useLoader(TextureLoader, "/assets/gripper.png"); // Blue side assets const player2FunnelXPositions = [1.2, 2.4, 3.6, 4.8]; const [player2FunnelX, setPlayer2FunnelX] = useState(player2FunnelXPositions[0]); const playerTwoActiveRef = useRef<number>(0); useEffect(() => { const interval = setInterval(() => { const randomIndex = Math.floor(Math.random() * player2FunnelXPositions.length); setPlayer2FunnelX(player2FunnelXPositions[randomIndex]); }, 500); // update every 0.5 seconds for a faster funnel movement return () => clearInterval(interval); }, []); // ✅ Define Robot Base Position const robotBasePosition: [number, number, number] = [.0, 1.0, 0]; // ✅ Define Arm Positions Relative to Base const robotArm1Position: [number, number, number] = [ robotBasePosition[0], robotBasePosition[1] - 0.5, 0, ]; const robotArm2Position: [number, number, number] = [ robotArm1Position[0], robotArm1Position[1] - 0.5, 0, ]; // ✅ Define Rail Position const railPosition: [number, number, number] = [3.0, 0.5, 0]; const userFunnelBaseY = 0.09; // ✅ Base Y-position for funnel (adjust as needed) // ✅ Define Constants for UserBox & UserFunnel Positioning const userFunnelBasePosition: [number, number, number] = [ player1VmXPositions[0], userFunnelBaseY, 0, ]; // ✅ Moves based on keypress // calculated values based on variables const [activeGpuIndex, setActiveGpuIndex] = useState(0); const playerOneMid: [number, number, number] = [ playerOneEnd[0], playerMidYPosition, 0, ]; const player2NextVmIndex = playerTwoTotal % 4; const player2NextXPosition = player2VmXPositions[player2NextVmIndex]; const playerTwoEnd: [number, number, number] = [ player2NextXPosition, vmYPositionBottom, 0, ]; const playerTwoMid: [number, number, number] = [ playerTwoEnd[0], playerMidYPosition, 0, ]; const playerOneActiveVmIndex = activeGpuIndex; // Use activeGpuIndex instead of finding position const playerOneAtMaxCapacity = vmStats.statusArray[playerOneActiveVmIndex].atMaxCapacity; const playerOneActiveVmId = playerOneActiveVmIndex + 1; const playerTwoActiveVmIndex = player2VmXPositions.reduce((bestIndex, pos, i) => { return Math.abs(pos - player2FunnelX) < Math.abs(player2VmXPositions[bestIndex] - player2FunnelX) ? i : bestIndex; }, 0); const playerTwoAtMaxCapacity = vmStats.statusArray[playerTwoActiveVmIndex + 4].atMaxCapacity; const playerOneScore = timeElapsed < 1 ? 0 : vmStats.playerOneScore; const playerTwoScore = timeElapsed < 1 ? 0 : vmStats.playerTwoScore; const timeRemaining = Math.min(totalGameTime - timeElapsed, totalGameTime); useEffect(() => { // add success block to any vm that has more than 0% in the queue const newSuccessXPosition = []; // add player one success block if (vmStats.statusArray[player2NextVmIndex].queue > 0) { newSuccessXPosition.push(player1VmXPositions[player2NextVmIndex]); } // add player two success block if (vmStats.statusArray[player2NextVmIndex + 4].queue > 0) { newSuccessXPosition.push(player2VmXPositions[player2NextVmIndex]); } const newSuccessBlocks = newSuccessXPosition.map((xPosition) => { const startingPosition: [number, number, number] = [ xPosition, vmYPosition + 0.42, -0.5, ]; const uid = crypto.randomUUID(); return { uid, startingPosition }; }); // limit to 100 blocks to prevent the screen from freezing up setSuccessBlocks([...newSuccessBlocks, ...successBlocks].slice(0, 20)); // eslint-disable-next-line react-hooks/exhaustive-deps }, [player2NextVmIndex]); useEffect(() => { const getStartLoader = async (playerName: string) => { try { setPlayerName(playerName); } catch (error) { console.error("Failed to start"); } }; if (!gameStarted && timeElapsed > -2) { setGameStarted(true); getStartLoader(playerName); } }, [gameStarted, playerName, timeElapsed]); useEffect(() => { const getVMStatus = async () => { if (timeElapsed < totalGameTime + 5) { // During GET READY (timeElapsed < 0), force GPU utilizations to 0. if (timeElapsed < 0) { const resetStats = { ...vmStats, statusArray: vmStats.statusArray.map((vm) => ({ ...vm, utilization: 0 })), playerOneScore: 0, playerTwoScore: 0, }; setVmStats(resetStats); return; } // Compute active right GPU index based on player2FunnelX: const playerTwoActiveVmIndex = player2GpuPositions.reduce((bestIndex, pos, i) => { return Math.abs(pos - player2FunnelX) < Math.abs(player2GpuPositions[bestIndex] - player2FunnelX) ? i : bestIndex; }, 0); // Update rates: const leftSideIncreaseRate = 0.035; const leftSideDecreaseRate = 0.01; const rightSideIncreaseRate = 0.08; // Increased active fill rate for right side const rightSideDecreaseRate = 0.02; // Decreased drain rate for inactive right GPUs const updatedVmStats = { ...vmStats, statusArray: vmStats.statusArray.map((vmDetails, index) => { if (index < 4) { // Left side GPUs. if (index === playerOneActiveVmIndex) { return { ...vmDetails, utilization: Math.min( vmDetails.utilization + leftSideIncreaseRate, 1 ), }; } else { return { ...vmDetails, utilization: Math.max(vmDetails.utilization - leftSideDecreaseRate, 0), }; } } else { // Right side GPUs (indices 4–7): compare (index - 4) with playerTwoActiveVmIndex. if ((index - 4) === playerTwoActiveVmIndex) { return { ...vmDetails, utilization: Math.min(vmDetails.utilization + rightSideIncreaseRate, 1), }; } else { return { ...vmDetails, utilization: Math.max(vmDetails.utilization - rightSideDecreaseRate, 0), }; } } }), playerOneScore: vmStats.playerOneScore + vmStats.statusArray.slice(0, 4).reduce((agg, vm) => agg + (vm.utilization > 0 ? 1 : 0), 0), playerTwoScore: vmStats.playerTwoScore + vmStats.statusArray.slice(4).reduce((agg, vm) => agg + (vm.utilization > 0 ? 1 : 0), 0), playerOneAtMaxCapacity: vmStats.statusArray[playerOneActiveVmIndex].utilization >= 1, playerTwoAtMaxCapacity: vmStats.statusArray[playerTwoActiveVmIndex + 4].utilization >= 1, }; setVmStats(updatedVmStats); } }; getVMStatus(); }, [quarterSecondCounter, timeElapsed, player2FunnelX]); // ✅ Define Constants for UserBox & UserFunnel Positioning // const userBoxPosition: [number, number, number] = [-2.9, 1.2, 0]; // ✅ Fixed position // ✅ State to Track Funnel Movement (Moves Horizontally) const [userFunnelPosition, setUserFunnelPosition] = useState< [number, number, number] >([ player1VmXPositions[0], // Starts at GPU 1 userFunnelBaseY, // Using the new base Y position 0, ]); // Add activeGpuIndex state to track which GPU is currently selected // Update the handleKeyDown function in your useEffect useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { let newXPosition: number | null = null; let newGpuIndex: number = activeGpuIndex; switch (event.code) { case "Digit1": newXPosition = player1VmXPositions[0]; newGpuIndex = 0; break; case "Digit2": newXPosition = player1VmXPositions[1]; newGpuIndex = 1; break; case "Digit3": newXPosition = player1VmXPositions[2]; newGpuIndex = 2; break; case "Digit4": newXPosition = player1VmXPositions[3]; newGpuIndex = 3; break; default: break; } if (newXPosition !== null) { setUserFunnelPosition([newXPosition, userFunnelBaseY, 0]); setActiveGpuIndex(newGpuIndex); } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, []); useEffect(() => { const playerName = generatePlayerName(); setPlayerName(playerName); }, []); useEffect(() => { //Implementing the setInterval method const interval = setInterval(() => { setTimeElapsed(timeElapsed + 1); }, 1000); //Clearing the interval return () => clearInterval(interval); }, [timeElapsed]); useEffect(() => { //Implementing the setInterval method const fastInterval = setInterval(() => { setQuarterSecondCounter(quarterSecondCounter + 1); }, 250); //Clearing the interval return () => clearInterval(fastInterval); }, [quarterSecondCounter]); useEffect(() => { if (timeElapsed >= totalGameTime && !showResults) { setShowResults(true); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [timeElapsed]); const addFailBlock = ( failBlockStartingPosition: [number, number, number] ) => { const [x, y, z] = failBlockStartingPosition; if (x < 0) { const audio = new Audio("/beep.wav"); audio.volume = 0.1; // audio.play(); } else { setTotalPlayerTwoTotal(playerTwoTotal + 1); } const startingPosition: [number, number, number] = [ x + Math.random() / 4 - 0.1, y, z, ]; const uid = crypto.randomUUID(); const side: "LEFT" | "RIGHT" = ["LEFT", "RIGHT"][ Math.floor(Math.random() * 2) ] as "LEFT" | "RIGHT"; const newBlock = { uid, startingPosition, side }; // limit to 100 blocks to prevent the screen from freezing up setFailBlocks([newBlock, ...failBlocks].slice(0, 10)); }; return ( <main className="flex h-screen flex-col items-center justify-between"> <div className="flex w-full overflow-clip justify-center text-7xl font-mono"> <div className="flex flex-row transition-all duration-1000 justify-end px-2 text-[#1a212c]" style={{ height: "80px", width: `${Math.max(playerOneScore, 50)}rem`, backgroundColor: colors.player1, }} > <h3 className={`text-7xl font-mono ${jersey15.className}`}> {playerOneScore.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")} </h3> </div> <div className="flex flex-row transition-all duration-1000 justify-start px-2 text-white" style={{ height: "80px", width: `${Math.max(playerTwoScore, 50)}rem`, backgroundColor: colors.player2, }} > <h3 className={`text-7xl font-mono ${jersey15.className}`}> {playerTwoScore.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")} </h3> </div> </div> <Canvas style={{ background: "white" }}> <ambientLight intensity={Math.PI / 2} /> <pointLight position={[-10, -10, -10]} decay={0} intensity={2} /> <pointLight position={[5, 5, 5]} decay={0} intensity={3} /> <Player1 showResults={false} /> <Player2 showResults={showResults} funnelX={player2FunnelX} /> <GpuGroup positions={player1GpuPositions} stats={vmStats.statusArray.slice(0, 4)} /> {/* Right side GPUs */} <GpuGroupRight positions={player2GpuPositions} stats={vmStats.statusArray.slice(4, 8)} /> </Canvas> <CountdownOverlay timeElapsed={timeElapsed} timeRemaining={timeRemaining} playerOneScore={playerOneScore} playerTwoScore={playerTwoScore} /> <ResultsOverlay showResults={showResults} playerOneScore={playerOneScore} playerTwoScore={playerTwoScore} playerName={playerName} /> </main> ); }