""" Data models for report generation. These are Python dataclasses, not database models. They represent the structure of report data in memory. """ from dataclasses import dataclass, field from typing import Dict, List, Optional from .spu_utils import ( calculate_total_cores, calculate_total_memory, extract_machine_generation, ) @dataclass class ClusterConfig: """Cluster configuration for a test.""" # Required fields (no defaults) tidb_spu: str tidb_count: int tikv_spu: str tikv_count: int # Optional fields (with defaults) cloud_provider: str = "AWS" region: str = "us-west-2" cluster_version: str = "" tidb_config: Optional[Dict] = None tikv_storage_type: Optional[str] = None tikv_storage_size_gi: Optional[int] = None tikv_config: Optional[Dict] = None pd_spu: Optional[str] = None pd_count: Optional[int] = None pd_config: Optional[Dict] = None tiflash_spu: Optional[str] = None tiflash_count: Optional[int] = None tiflash_storage_type: Optional[str] = None tiflash_storage_size_gi: Optional[int] = None tiflash_config: Optional[Dict] = None tiproxy_spu: Optional[str] = None tiproxy_count: Optional[int] = None workload_config: Optional[Dict] = None tiproxy_config: Optional[Dict] = None # Computed properties for filtering and comparison @property def tidb_machine_generation(self) -> Optional[str]: """Extract machine generation from TiDB SPU (e.g., 'G2', 'G4').""" return extract_machine_generation(self.tidb_spu) @property def tikv_machine_generation(self) -> Optional[str]: """Extract machine generation from TiKV SPU.""" return extract_machine_generation(self.tikv_spu) @property def tidb_total_cores(self) -> int: """Calculate total TiDB cores.""" return calculate_total_cores(self.tidb_spu, self.tidb_count) @property def tidb_total_memory(self) -> int: """Calculate total TiDB memory in GB.""" return calculate_total_memory(self.tidb_spu, self.tidb_count) @property def tikv_total_cores(self) -> int: """Calculate total TiKV cores.""" return calculate_total_cores(self.tikv_spu, self.tikv_count) @property def tikv_total_memory(self) -> int: """Calculate total TiKV memory in GB.""" return calculate_total_memory(self.tikv_spu, self.tikv_count) @property def tiproxy_enabled(self) -> bool: """Check if TiProxy is enabled.""" return bool(self.tiproxy_spu and self.tiproxy_count) @dataclass class WorkloadRunData: """Data for a single workload run.""" node_name: str workload_type: str # "tpcc", "sysbench-read_write", etc. params: Dict # {"thread": 100, "duration": 120, "warehouses": 1000} metrics: Dict # {"tpmC": 7508.7, "latency": {"p95": 104.9}} status: str # "SUCCEEDED", "FAILED" task_id: Optional[int] = None workflow_execution_id: Optional[str] = None started_at: Optional[str] = None completed_at: Optional[str] = None client_metrics: Optional[Dict] = ( None # Client resource metrics (CPU, Memory, Network) ) @dataclass class WorkflowData: """Data for a workflow execution (e.g., bench-tpcc).""" workflow_type: str # "bench-tpcc", "bench-sysbench", etc. workflow_execution_id: str task_id: int status: str runs: List[WorkloadRunData] = field(default_factory=list) @dataclass class SPUReportData: """Complete report data for a single SPU configuration.""" spu_identifier: str cluster_config: ClusterConfig workflows: List[WorkflowData] = field(default_factory=list) def get_workflow(self, workflow_type: str) -> Optional[WorkflowData]: """Get workflow by type.""" for wf in self.workflows: if wf.workflow_type == workflow_type: return wf return None def get_all_runs(self, workflow_type: str) -> List[WorkloadRunData]: """Get all runs for a specific workflow type.""" runs = [] for wf in self.workflows: if wf.workflow_type == workflow_type: runs.extend(wf.runs) return runs @dataclass class PerformanceCurveData: """Performance curve data for charting.""" thread_counts: List[int] = field(default_factory=list) tps_values: List[float] = field(default_factory=list) p95_values: List[float] = field(default_factory=list) p99_values: List[float] = field(default_factory=list) def add_point( self, thread_count: int, tps: Optional[float] = None, p95: Optional[float] = None, p99: Optional[float] = None, ): """Add a data point to the curve.""" self.thread_counts.append(thread_count) self.tps_values.append(tps if tps is not None else 0) self.p95_values.append(p95 if p95 is not None else 0) self.p99_values.append(p99 if p99 is not None else 0) @dataclass class MetricStats: """Statistical summary for a metric.""" avg: float = 0.0 median: float = 0.0 std: float = 0.0 min: float = 0.0 max: float = 0.0 @dataclass class TaskComparisonRun: """Data for a single run in comparison.""" task_id: int run: WorkloadRunData # Contains all metrics, params, client_metrics @dataclass class ThreadComparisonData: """Comparison data for a specific thread count.""" thread_count: int # Config 1 runs for this thread count config1_runs: List[TaskComparisonRun] = field(default_factory=list) # Config 2 runs for this thread count config2_runs: List[TaskComparisonRun] = field(default_factory=list) # Summary statistics for each metric # Format: {metric_name: (config1_stats, config2_stats, improvement%)} summary_stats: Dict[str, tuple[MetricStats, MetricStats, float]] = field( default_factory=dict ) @dataclass class ComparisonData: """Comparison data between two SPU configurations.""" workflow_type: str spu_1_identifier: str spu_2_identifier: str # Thread count -> ThreadComparisonData thread_data: Dict[int, ThreadComparisonData] = field(default_factory=dict) # Overall average improvement across all metrics (DEPRECATED - use metric_improvements instead) average_improvement: float = 0.0 # Average improvement per metric (averaged across all thread counts) # Format: {metric_name: average_improvement%} # Example: {"tpmC": 10.5, "latency.p95": -5.2, "latency.p99": -3.8} metric_improvements: Dict[str, float] = field(default_factory=dict) # Legacy fields for backward compatibility (deprecated) # Thread count -> (spu1_tps, spu2_tps, improvement%) tps_comparison: Dict[int, tuple[float, float, float]] = field(default_factory=dict) # Thread count -> (spu1_p95, spu2_p95, improvement%) p95_comparison: Dict[int, tuple[float, float, float]] = field(default_factory=dict) @dataclass class SPUComparisonReport: """Comparison report between two SPU configurations.""" spu_1: SPUReportData spu_2: SPUReportData comparisons: List[ComparisonData] = field(default_factory=list) def get_comparison(self, workflow_type: str) -> Optional[ComparisonData]: """Get comparison for a specific workflow type.""" for comp in self.comparisons: if comp.workflow_type == workflow_type: return comp return None