Source

utils/debugTools.js

/**
 * @fileoverview Debug and performance monitoring tools
 * @module DebugTools
 */

/**
 * Performance monitor class
 */
export class PerformanceMonitor {
  constructor() {
    this.metrics = {
      fps: 0,
      frameTime: 0,
      memoryUsage: 0,
      particleCount: 0,
      drawCalls: 0,
      averageFPS: 0,
      minFPS: Infinity,
      maxFPS: 0
    };
    
    this.frameHistory = [];
    this.maxHistoryLength = 60; // 1 second at 60fps
    this.lastFrameTime = 0;
    this.frameCount = 0;
    this.startTime = performance.now();
    
    this.thresholds = {
      lowFPS: 30,
      highMemory: 100 * 1024 * 1024, // 100MB
      highParticles: 1000
    };
    
    this.alerts = [];
    this.onAlert = null;
  }

  /**
   * Update performance metrics
   * @param {Object} data - Frame data
   */
  update(data = {}) {
    const now = performance.now();
    const deltaTime = now - this.lastFrameTime;
    
    if (this.lastFrameTime > 0) {
      const fps = 1000 / deltaTime;
      this.metrics.fps = fps;
      this.metrics.frameTime = deltaTime;
      
      // Update FPS history
      this.frameHistory.push(fps);
      if (this.frameHistory.length > this.maxHistoryLength) {
        this.frameHistory.shift();
      }
      
      // Calculate average FPS
      this.metrics.averageFPS = this.frameHistory.reduce((sum, f) => sum + f, 0) / this.frameHistory.length;
      this.metrics.minFPS = Math.min(this.metrics.minFPS, fps);
      this.metrics.maxFPS = Math.max(this.metrics.maxFPS, fps);
      
      // Update other metrics
      if (data.particleCount !== undefined) {
        this.metrics.particleCount = data.particleCount;
      }
      
      if (data.drawCalls !== undefined) {
        this.metrics.drawCalls = data.drawCalls;
      }
      
      // Memory usage
      if (performance.memory) {
        this.metrics.memoryUsage = performance.memory.usedJSHeapSize;
      }
      
      // Check for performance issues
      this.checkPerformanceAlerts();
    }
    
    this.lastFrameTime = now;
    this.frameCount++;
  }

  /**
   * Check for performance alerts
   */
  checkPerformanceAlerts() {
    const alerts = [];
    
    if (this.metrics.fps < this.thresholds.lowFPS) {
      alerts.push({
        type: 'warning',
        message: `Low FPS detected: ${this.metrics.fps.toFixed(1)}`,
        metric: 'fps',
        value: this.metrics.fps,
        threshold: this.thresholds.lowFPS
      });
    }
    
    if (this.metrics.memoryUsage > this.thresholds.highMemory) {
      alerts.push({
        type: 'warning',
        message: `High memory usage: ${(this.metrics.memoryUsage / 1024 / 1024).toFixed(1)}MB`,
        metric: 'memory',
        value: this.metrics.memoryUsage,
        threshold: this.thresholds.highMemory
      });
    }
    
    if (this.metrics.particleCount > this.thresholds.highParticles) {
      alerts.push({
        type: 'info',
        message: `High particle count: ${this.metrics.particleCount}`,
        metric: 'particles',
        value: this.metrics.particleCount,
        threshold: this.thresholds.highParticles
      });
    }
    
    if (alerts.length > 0 && this.onAlert) {
      this.onAlert(alerts);
    }
    
    this.alerts = alerts;
  }

  /**
   * Get performance report
   * @returns {Object} Performance report
   */
  getReport() {
    const runTime = performance.now() - this.startTime;
    
    return {
      ...this.metrics,
      runTime,
      totalFrames: this.frameCount,
      alerts: this.alerts,
      recommendations: this.getRecommendations()
    };
  }

  /**
   * Get performance recommendations
   * @returns {Array} Recommendations
   */
  getRecommendations() {
    const recommendations = [];
    
    if (this.metrics.averageFPS < 45) {
      recommendations.push({
        type: 'performance',
        message: 'Consider reducing particle count or enabling performance optimizations',
        action: 'reduce_particles'
      });
    }
    
    if (this.metrics.memoryUsage > 50 * 1024 * 1024) {
      recommendations.push({
        type: 'memory',
        message: 'High memory usage detected. Consider enabling garbage collection optimizations',
        action: 'optimize_memory'
      });
    }
    
    if (this.metrics.drawCalls > 1000) {
      recommendations.push({
        type: 'rendering',
        message: 'High draw call count. Consider using instanced rendering or batch processing',
        action: 'optimize_rendering'
      });
    }
    
    return recommendations;
  }

  /**
   * Reset metrics
   */
  reset() {
    this.frameHistory = [];
    this.frameCount = 0;
    this.startTime = performance.now();
    this.lastFrameTime = 0;
    this.metrics.minFPS = Infinity;
    this.metrics.maxFPS = 0;
    this.alerts = [];
  }

  /**
   * Set performance thresholds
   * @param {Object} thresholds - New thresholds
   */
  setThresholds(thresholds) {
    Object.assign(this.thresholds, thresholds);
  }
}

/**
 * Debug visualizer for rendering debug information
 */
export class DebugVisualizer {
  constructor(canvas) {
    this.canvas = canvas;
    this.ctx = canvas.getContext('2d');
    this.enabled = false;
    this.showFPS = true;
    this.showParticles = true;
    this.showMemory = true;
    this.showGrid = false;
    this.showBounds = false;
    this.showForces = false;
    
    this.gridSize = 50;
    this.fontSize = 14;
    this.textColor = '#ffffff';
    this.gridColor = '#333333';
    this.boundsColor = '#ff0000';
    this.forceColor = '#00ff00';
  }

  /**
   * Enable debug visualization
   */
  enable() {
    this.enabled = true;
  }

  /**
   * Disable debug visualization
   */
  disable() {
    this.enabled = false;
  }

  /**
   * Toggle debug features
   * @param {string} feature - Feature to toggle
   */
  toggle(feature) {
    if (this.hasOwnProperty(feature)) {
      this[feature] = !this[feature];
    }
  }

  /**
   * Render debug information
   * @param {Object} debugData - Debug data to display
   */
  render(debugData = {}) {
    if (!this.enabled) return;
    
    const ctx = this.ctx;
    ctx.save();
    
    // Set up text rendering
    ctx.font = `${this.fontSize}px monospace`;
    ctx.fillStyle = this.textColor;
    ctx.strokeStyle = '#000000';
    ctx.lineWidth = 3;
    
    let yOffset = 30;
    const lineHeight = this.fontSize + 5;
    
    // Draw performance metrics
    if (this.showFPS && debugData.performance) {
      const perf = debugData.performance;
      this.drawText(`FPS: ${perf.fps.toFixed(1)} (avg: ${perf.averageFPS.toFixed(1)})`, 10, yOffset);
      yOffset += lineHeight;
      
      this.drawText(`Frame Time: ${perf.frameTime.toFixed(2)}ms`, 10, yOffset);
      yOffset += lineHeight;
    }
    
    // Draw particle information
    if (this.showParticles && debugData.particles) {
      const particles = debugData.particles;
      this.drawText(`Particles: ${particles.count} (active: ${particles.active})`, 10, yOffset);
      yOffset += lineHeight;
      
      if (particles.types) {
        Object.entries(particles.types).forEach(([type, count]) => {
          this.drawText(`  ${type}: ${count}`, 10, yOffset);
          yOffset += lineHeight;
        });
      }
    }
    
    // Draw memory information
    if (this.showMemory && debugData.memory) {
      const memory = debugData.memory;
      this.drawText(`Memory: ${(memory.used / 1024 / 1024).toFixed(1)}MB / ${(memory.total / 1024 / 1024).toFixed(1)}MB`, 10, yOffset);
      yOffset += lineHeight;
    }
    
    // Draw grid
    if (this.showGrid) {
      this.drawGrid();
    }
    
    // Draw bounds
    if (this.showBounds && debugData.bounds) {
      this.drawBounds(debugData.bounds);
    }
    
    // Draw force vectors
    if (this.showForces && debugData.forces) {
      this.drawForces(debugData.forces);
    }
    
    // Draw alerts
    if (debugData.alerts && debugData.alerts.length > 0) {
      this.drawAlerts(debugData.alerts, yOffset);
    }
    
    ctx.restore();
  }

  /**
   * Draw text with outline
   * @param {string} text - Text to draw
   * @param {number} x - X position
   * @param {number} y - Y position
   */
  drawText(text, x, y) {
    const ctx = this.ctx;
    ctx.strokeText(text, x, y);
    ctx.fillText(text, x, y);
  }

  /**
   * Draw debug grid
   */
  drawGrid() {
    const ctx = this.ctx;
    const { width, height } = this.canvas;
    
    ctx.save();
    ctx.strokeStyle = this.gridColor;
    ctx.lineWidth = 1;
    ctx.setLineDash([2, 2]);
    
    // Vertical lines
    for (let x = 0; x <= width; x += this.gridSize) {
      ctx.beginPath();
      ctx.moveTo(x, 0);
      ctx.lineTo(x, height);
      ctx.stroke();
    }
    
    // Horizontal lines
    for (let y = 0; y <= height; y += this.gridSize) {
      ctx.beginPath();
      ctx.moveTo(0, y);
      ctx.lineTo(width, y);
      ctx.stroke();
    }
    
    ctx.restore();
  }

  /**
   * Draw boundary boxes
   * @param {Array} bounds - Array of boundary objects
   */
  drawBounds(bounds) {
    const ctx = this.ctx;
    
    ctx.save();
    ctx.strokeStyle = this.boundsColor;
    ctx.lineWidth = 2;
    ctx.setLineDash([5, 5]);
    
    bounds.forEach(bound => {
      ctx.strokeRect(bound.x, bound.y, bound.width, bound.height);
    });
    
    ctx.restore();
  }

  /**
   * Draw force vectors
   * @param {Array} forces - Array of force objects
   */
  drawForces(forces) {
    const ctx = this.ctx;
    
    ctx.save();
    ctx.strokeStyle = this.forceColor;
    ctx.lineWidth = 2;
    
    forces.forEach(force => {
      const { x, y, fx, fy, strength = 1 } = force;
      const length = Math.min(strength * 50, 100);
      const angle = Math.atan2(fy, fx);
      
      const endX = x + Math.cos(angle) * length;
      const endY = y + Math.sin(angle) * length;
      
      // Draw force vector
      ctx.beginPath();
      ctx.moveTo(x, y);
      ctx.lineTo(endX, endY);
      ctx.stroke();
      
      // Draw arrowhead
      const arrowLength = 10;
      const arrowAngle = 0.5;
      
      ctx.beginPath();
      ctx.moveTo(endX, endY);
      ctx.lineTo(
        endX - arrowLength * Math.cos(angle - arrowAngle),
        endY - arrowLength * Math.sin(angle - arrowAngle)
      );
      ctx.moveTo(endX, endY);
      ctx.lineTo(
        endX - arrowLength * Math.cos(angle + arrowAngle),
        endY - arrowLength * Math.sin(angle + arrowAngle)
      );
      ctx.stroke();
    });
    
    ctx.restore();
  }

  /**
   * Draw performance alerts
   * @param {Array} alerts - Array of alert objects
   * @param {number} yOffset - Y offset for positioning
   */
  drawAlerts(alerts, yOffset) {
    const ctx = this.ctx;
    const lineHeight = this.fontSize + 5;
    
    ctx.save();
    
    alerts.forEach((alert, index) => {
      const y = yOffset + index * lineHeight;
      
      // Set color based on alert type
      switch (alert.type) {
        case 'error':
          ctx.fillStyle = '#ff4444';
          break;
        case 'warning':
          ctx.fillStyle = '#ffaa44';
          break;
        case 'info':
          ctx.fillStyle = '#4444ff';
          break;
        default:
          ctx.fillStyle = this.textColor;
      }
      
      this.drawText(`⚠ ${alert.message}`, 10, y);
    });
    
    ctx.restore();
  }
}

/**
 * Debug console for logging and command execution
 */
export class DebugConsole {
  constructor() {
    this.logs = [];
    this.maxLogs = 1000;
    this.commands = new Map();
    this.enabled = false;
    
    this.setupCommands();
  }

  /**
   * Setup built-in debug commands
   */
  setupCommands() {
    this.addCommand('help', () => {
      this.log('Available commands:');
      this.commands.forEach((_, name) => {
        this.log(`  ${name}`);
      });
    });
    
    this.addCommand('clear', () => {
      this.logs = [];
      this.log('Console cleared');
    });
    
    this.addCommand('fps', (manager) => {
      if (manager && manager.performanceMonitor) {
        const metrics = manager.performanceMonitor.getReport();
        this.log(`FPS: ${metrics.fps.toFixed(1)} (avg: ${metrics.averageFPS.toFixed(1)})`);
      }
    });
    
    this.addCommand('memory', () => {
      if (performance.memory) {
        const used = (performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(1);
        const total = (performance.memory.totalJSHeapSize / 1024 / 1024).toFixed(1);
        this.log(`Memory: ${used}MB / ${total}MB`);
      }
    });
    
    this.addCommand('particles', (manager) => {
      if (manager && manager.instance) {
        const count = manager.instance.getParticleCount ? manager.instance.getParticleCount() : 'Unknown';
        this.log(`Particle count: ${count}`);
      }
    });
  }

  /**
   * Add custom debug command
   * @param {string} name - Command name
   * @param {Function} handler - Command handler
   */
  addCommand(name, handler) {
    this.commands.set(name, handler);
  }

  /**
   * Execute debug command
   * @param {string} command - Command string
   * @param {*} context - Execution context
   */
  execute(command, context = null) {
    const parts = command.trim().split(' ');
    const commandName = parts[0];
    const args = parts.slice(1);
    
    if (this.commands.has(commandName)) {
      try {
        this.commands.get(commandName)(context, ...args);
      } catch (error) {
        this.error(`Command error: ${error.message}`);
      }
    } else {
      this.error(`Unknown command: ${commandName}`);
    }
  }

  /**
   * Log message
   * @param {string} message - Log message
   * @param {string} level - Log level
   */
  log(message, level = 'info') {
    const timestamp = new Date().toISOString();
    const logEntry = {
      timestamp,
      level,
      message
    };
    
    this.logs.push(logEntry);
    
    if (this.logs.length > this.maxLogs) {
      this.logs.shift();
    }
    
    if (this.enabled) {
      console.log(`[${timestamp}] ${level.toUpperCase()}: ${message}`);
    }
  }

  /**
   * Log error
   * @param {string} message - Error message
   */
  error(message) {
    this.log(message, 'error');
  }

  /**
   * Log warning
   * @param {string} message - Warning message
   */
  warn(message) {
    this.log(message, 'warn');
  }

  /**
   * Log debug message
   * @param {string} message - Debug message
   */
  debug(message) {
    this.log(message, 'debug');
  }

  /**
   * Get filtered logs
   * @param {string} level - Filter by level
   * @returns {Array} Filtered logs
   */
  getLogs(level = null) {
    if (level) {
      return this.logs.filter(log => log.level === level);
    }
    return [...this.logs];
  }

  /**
   * Export logs
   * @returns {string} Exported log data
   */
  exportLogs() {
    return JSON.stringify(this.logs, null, 2);
  }

  /**
   * Enable console output
   */
  enable() {
    this.enabled = true;
  }

  /**
   * Disable console output
   */
  disable() {
    this.enabled = false;
  }
}

/**
 * Unified debug manager
 */
export class DebugManager {
  constructor(canvas) {
    this.performanceMonitor = new PerformanceMonitor();
    this.visualizer = new DebugVisualizer(canvas);
    this.console = new DebugConsole();
    this.enabled = false;
    
    this.setupKeyboardShortcuts();
  }

  /**
   * Setup keyboard shortcuts for debug controls
   */
  setupKeyboardShortcuts() {
    document.addEventListener('keydown', (event) => {
      if (!this.enabled) return;
      
      if (event.ctrlKey || event.metaKey) {
        switch (event.key) {
          case 'd':
            event.preventDefault();
            this.visualizer.enabled = !this.visualizer.enabled;
            this.console.log(`Debug visualizer ${this.visualizer.enabled ? 'enabled' : 'disabled'}`);
            break;
          case 'g':
            event.preventDefault();
            this.visualizer.toggle('showGrid');
            this.console.log(`Grid display ${this.visualizer.showGrid ? 'enabled' : 'disabled'}`);
            break;
          case 'p':
            event.preventDefault();
            this.visualizer.toggle('showParticles');
            this.console.log(`Particle info ${this.visualizer.showParticles ? 'enabled' : 'disabled'}`);
            break;
          case 'f':
            event.preventDefault();
            this.visualizer.toggle('showFPS');
            this.console.log(`FPS display ${this.visualizer.showFPS ? 'enabled' : 'disabled'}`);
            break;
        }
      }
    });
  }

  /**
   * Enable debug mode
   */
  enable() {
    this.enabled = true;
    this.console.enable();
    this.console.log('Debug mode enabled');
    this.console.log('Keyboard shortcuts:');
    this.console.log('  Ctrl+D: Toggle debug visualizer');
    this.console.log('  Ctrl+G: Toggle grid');
    this.console.log('  Ctrl+P: Toggle particle info');
    this.console.log('  Ctrl+F: Toggle FPS display');
  }

  /**
   * Disable debug mode
   */
  disable() {
    this.enabled = false;
    this.visualizer.disable();
    this.console.disable();
  }

  /**
   * Update debug information
   * @param {Object} data - Debug data
   */
  update(data = {}) {
    if (!this.enabled) return;
    
    this.performanceMonitor.update(data.performance);
    
    const debugData = {
      performance: this.performanceMonitor.getReport(),
      particles: data.particles,
      memory: performance.memory ? {
        used: performance.memory.usedJSHeapSize,
        total: performance.memory.totalJSHeapSize
      } : null,
      bounds: data.bounds,
      forces: data.forces,
      alerts: this.performanceMonitor.alerts
    };
    
    this.visualizer.render(debugData);
  }

  /**
   * Get comprehensive debug report
   * @returns {Object} Debug report
   */
  getReport() {
    return {
      performance: this.performanceMonitor.getReport(),
      console: this.console.getLogs(),
      visualizer: {
        enabled: this.visualizer.enabled,
        features: {
          showFPS: this.visualizer.showFPS,
          showParticles: this.visualizer.showParticles,
          showMemory: this.visualizer.showMemory,
          showGrid: this.visualizer.showGrid,
          showBounds: this.visualizer.showBounds,
          showForces: this.visualizer.showForces
        }
      }
    };
  }
}

// Export singleton instance creator
export function createDebugManager(canvas) {
  return new DebugManager(canvas);
}