d3-force-webgpu
Version:
GPU-accelerated force-directed graph layout with adaptive CPU/GPU selection. Drop-in replacement for d3-force with WebGPU support.
282 lines (223 loc) • 8.13 kB
JavaScript
import cpuSimulation from "./simulation.js";
import forceSimulationGPU from "./webgpu/simulation-gpu.js";
// Performance thresholds based on typical benchmarks
const PERFORMANCE_THRESHOLDS = {
// Below this node count, CPU is usually faster due to GPU overhead
MIN_GPU_NODES: 750,
// If CPU is significantly slower than this FPS, consider GPU
MIN_CPU_FPS: 30,
// If link count exceeds this, GPU becomes more beneficial
MIN_GPU_LINKS: 1000,
// Complexity score threshold (nodes * links)
COMPLEXITY_THRESHOLD: 500000
};
// Auto-detection logic
function shouldUseGPU(nodes, estimatedLinkCount = nodes.length * 2) {
const nodeCount = nodes.length;
const complexity = nodeCount * estimatedLinkCount;
// WebGPU not available
if (!navigator.gpu) {
return false;
}
// Too few nodes - GPU overhead not worth it
if (nodeCount < PERFORMANCE_THRESHOLDS.MIN_GPU_NODES) {
return false;
}
// High complexity - GPU likely beneficial
if (complexity > PERFORMANCE_THRESHOLDS.COMPLEXITY_THRESHOLD) {
return true;
}
// Medium complexity - GPU might be beneficial
if (nodeCount >= PERFORMANCE_THRESHOLDS.MIN_GPU_NODES &&
estimatedLinkCount >= PERFORMANCE_THRESHOLDS.MIN_GPU_LINKS) {
return true;
}
return false;
}
// Performance monitor to switch implementations dynamically
class PerformanceMonitor {
constructor() {
this.fpsHistory = [];
this.frameTimeHistory = [];
this.lastTime = performance.now();
this.frameCount = 0;
this.avgFPS = 0;
this.avgFrameTime = 0;
}
recordFrame(frameTime) {
this.frameTimeHistory.push(frameTime);
this.frameCount++;
const now = performance.now();
if (now - this.lastTime >= 1000) {
const fps = Math.round(this.frameCount * 1000 / (now - this.lastTime));
this.fpsHistory.push(fps);
this.frameCount = 0;
this.lastTime = now;
// Calculate rolling averages
const recentFPS = this.fpsHistory.slice(-5); // Last 5 seconds
const recentFrameTimes = this.frameTimeHistory.slice(-300); // Last ~5 seconds at 60fps
this.avgFPS = recentFPS.reduce((a, b) => a + b, 0) / recentFPS.length;
this.avgFrameTime = recentFrameTimes.reduce((a, b) => a + b, 0) / recentFrameTimes.length;
}
}
shouldSwitchToGPU() {
return this.avgFPS < PERFORMANCE_THRESHOLDS.MIN_CPU_FPS && this.fpsHistory.length >= 3;
}
reset() {
this.fpsHistory = [];
this.frameTimeHistory = [];
this.frameCount = 0;
this.lastTime = performance.now();
}
}
// Adaptive force simulation that chooses the best implementation
export default async function forceSimulationAdaptive(nodes, options = {}) {
const {
mode = 'auto', // 'auto', 'cpu', 'gpu', 'hybrid'
enableSwitching = true, // Allow dynamic switching based on performance
onModeChange = null, // Callback when mode changes
estimatedLinkCount = nodes?.length * 2
} = options;
let currentMode = mode;
let currentSimulation = null;
let monitor = new PerformanceMonitor();
let switchCheckInterval = null;
// Determine initial mode
if (mode === 'auto') {
currentMode = shouldUseGPU(nodes, estimatedLinkCount) ? 'gpu' : 'cpu';
}
async function createSimulation(nodes, useGPU = false) {
if (useGPU) {
try {
const sim = forceSimulationGPU(nodes);
await sim.initialize?.();
return { simulation: sim, type: 'gpu', initialized: true };
} catch (error) {
console.warn('GPU simulation failed, falling back to CPU:', error.message);
return { simulation: cpuSimulation(nodes), type: 'cpu', initialized: true };
}
} else {
return { simulation: cpuSimulation(nodes), type: 'cpu', initialized: true };
}
}
async function switchToGPU() {
if (currentMode === 'gpu' || !enableSwitching) return;
console.log('🚀 Switching to GPU acceleration for better performance');
const oldSim = currentSimulation.simulation;
const forces = new Map();
// Preserve force configuration
if (oldSim.forces) {
oldSim.forces.forEach((force, name) => {
forces.set(name, force);
});
}
// Stop old simulation
oldSim.stop();
// Create new GPU simulation
const newSim = await createSimulation(nodes, true);
// Transfer forces
forces.forEach((force, name) => {
newSim.simulation.force(name, force);
});
// Update references
currentSimulation = newSim;
currentMode = 'gpu';
monitor.reset();
if (onModeChange) {
onModeChange('gpu', 'Switched to GPU for better performance');
}
return newSim.simulation;
}
// Create initial simulation
const initialSim = await createSimulation(nodes, currentMode === 'gpu');
currentSimulation = initialSim;
// Wrap simulation to add performance monitoring
const originalTick = currentSimulation.simulation.tick;
currentSimulation.simulation.tick = function(...args) {
const startTime = performance.now();
const result = originalTick.apply(this, args);
const frameTime = performance.now() - startTime;
monitor.recordFrame(frameTime);
return result;
};
// Set up performance monitoring for dynamic switching
if (enableSwitching && currentMode === 'cpu') {
switchCheckInterval = setInterval(async () => {
if (monitor.shouldSwitchToGPU()) {
clearInterval(switchCheckInterval);
await switchToGPU();
}
}, 2000); // Check every 2 seconds
}
// Extend simulation with adaptive methods
const adaptiveSimulation = currentSimulation.simulation;
// Add adaptive-specific methods
adaptiveSimulation.getCurrentMode = () => currentMode;
adaptiveSimulation.getPerformanceStats = () => ({
avgFPS: monitor.avgFPS,
avgFrameTime: monitor.avgFrameTime,
mode: currentMode
});
adaptiveSimulation.forceMode = async (newMode) => {
if (newMode === currentMode) return adaptiveSimulation;
const oldSim = currentSimulation.simulation;
const forces = new Map();
// Preserve configuration
if (oldSim.forces) {
oldSim.forces.forEach((force, name) => {
forces.set(name, force);
});
}
oldSim.stop();
// Create new simulation
const newSim = await createSimulation(nodes, newMode === 'gpu');
// Transfer forces
forces.forEach((force, name) => {
newSim.simulation.force(name, force);
});
currentSimulation = newSim;
currentMode = newMode;
monitor.reset();
if (onModeChange) {
onModeChange(newMode, `Manually switched to ${newMode.toUpperCase()}`);
}
return newSim.simulation;
};
// Override stop to clean up monitoring
const originalStop = adaptiveSimulation.stop;
adaptiveSimulation.stop = function() {
if (switchCheckInterval) {
clearInterval(switchCheckInterval);
switchCheckInterval = null;
}
return originalStop.call(this);
};
// Add mode information to simulation
adaptiveSimulation._adaptiveMode = currentMode;
adaptiveSimulation._isAdaptive = true;
if (onModeChange) {
onModeChange(currentMode, `Started with ${currentMode.toUpperCase()} mode`);
}
return adaptiveSimulation;
}
// Export configuration utilities
export const AdaptiveConfig = {
setThresholds: (newThresholds) => {
Object.assign(PERFORMANCE_THRESHOLDS, newThresholds);
},
getThresholds: () => ({ ...PERFORMANCE_THRESHOLDS }),
shouldUseGPU,
estimateComplexity: (nodeCount, linkCount) => nodeCount * linkCount,
recommendMode: (nodes, links = []) => {
const complexity = nodes.length * links.length;
if (!navigator.gpu) return 'cpu';
if (nodes.length < 200) return 'cpu';
if (complexity > 1000000) return 'gpu';
if (nodes.length > 2000) return 'gpu';
return 'auto';
}
};
// Convenience exports for specific modes
export const forceCPU = cpuSimulation;
export const forceGPU = forceSimulationGPU;
export const forceAuto = forceSimulationAdaptive;