""" Performance metrics parser. Parses performance metrics from workload execution summaries. """ import re from typing import Dict, Optional from src.services.workload_templates import is_premium_workload_config def parse_performance_metrics( summary: str, workload_type: str, node_name: str = "" ) -> Dict: """Parse performance metrics from summary text. Supports three workload types: - tpcc: TPC-C benchmark (OLTP) - tpch (bench-ch): TPC-H benchmark (hybrid OLTP + OLAP) - sysbench: Sysbench with variants (read, write, mix) Args: summary: Summary text from workload execution workload_type: Type of workload (bench-tpcc, bench-ch, bench-sysbench) node_name: Node name for additional context Returns: Dictionary of performance metrics """ if not summary: return {} # Determine workload family # Note: Only three workload types exist: tpcc, tpch (bench-ch), sysbench if "tpcc" in workload_type.lower() or "tpcc" in node_name.lower(): return parse_tpcc_metrics(summary) elif "sysbench" in workload_type.lower() or "oltp" in node_name.lower(): return parse_sysbench_metrics(summary, node_name) elif "bench-ch" in workload_type.lower() or "tpch" in workload_type.lower(): # bench-ch is TPC-H test (hybrid OLTP + OLAP) return parse_tpch_metrics(summary) else: return {} def parse_tpcc_metrics(summary: str) -> Dict: """Parse TPCC performance metrics. TPCC summary format example: ``` Finished [Summary] DELIVERY - Takes(s): 7199.8, Count: 901013, TPM: 7508.7, Avg(ms): 84.2, 50th(ms): 83.9, 90th(ms): 100.7, 95th(ms): 104.9, 99th(ms): 125.8, Max(ms): 192.9 [Summary] NEW_ORDER - Takes(s): 7199.8, Count: 10150345, TPM: 84599.5, Avg(ms): 45.7, 50th(ms): 46.1, 90th(ms): 52.4, 95th(ms): 56.6, 99th(ms): 79.7, Max(ms): 536.9 ``` """ metrics = {} # Extract NEW_ORDER TPM (this is the tpmC metric) new_order_match = re.search( r"\[Summary\]\s+NEW_ORDER.*?TPM:\s*([\d.]+)", summary, re.DOTALL ) if new_order_match: tpm = float(new_order_match.group(1)) metrics["tpmC"] = tpm metrics["tps"] = tpm / 60 # Convert TPM to TPS # Extract latency metrics for each transaction type txn_types = ["DELIVERY", "NEW_ORDER", "ORDER_STATUS", "PAYMENT", "STOCK_LEVEL"] for txn_type in txn_types: pattern = ( rf"\[Summary\]\s+{txn_type}.*?" r"Avg\(ms\):\s*([\d.]+).*?" r"50th\(ms\):\s*([\d.]+).*?" r"90th\(ms\):\s*([\d.]+).*?" r"95th\(ms\):\s*([\d.]+).*?" r"99th\(ms\):\s*([\d.]+).*?" r"Max\(ms\):\s*([\d.]+)" ) match = re.search(pattern, summary, re.DOTALL) if match: metrics[txn_type.lower()] = { "avg": float(match.group(1)), "p50": float(match.group(2)), "p90": float(match.group(3)), "p95": float(match.group(4)), "p99": float(match.group(5)), "max": float(match.group(6)), } # Calculate overall latency if NEW_ORDER exists (most common transaction) if "new_order" in metrics: metrics["latency"] = { "avg": metrics["new_order"]["avg"], "p50": metrics["new_order"]["p50"], "p90": metrics["new_order"]["p90"], "p95": metrics["new_order"]["p95"], "p99": metrics["new_order"]["p99"], "max": metrics["new_order"]["max"], } return metrics def parse_sysbench_metrics(summary: str, node_name: str = "") -> Dict: """Parse Sysbench performance metrics. Sysbench summary format varies by workload type: - oltp_read_write, oltp_point_select: transactions/sec, latency - oltp_insert, oltp_update: queries/sec, latency """ metrics = {} # Try to extract TPS (transactions per second) tps_patterns = [ r"transactions:\s+\d+\s+\(([\d.]+)\s+per\s+sec", r"(\d+\.\d+)\s+transactions/sec", ] for pattern in tps_patterns: match = re.search(pattern, summary, re.IGNORECASE) if match: metrics["tps"] = float(match.group(1)) break # Try to extract QPS (queries per second) qps_patterns = [ r"queries:\s+\d+\s+\(([\d.]+)\s+per\s+sec", r"(\d+\.\d+)\s+queries/sec", ] for pattern in qps_patterns: match = re.search(pattern, summary, re.IGNORECASE) if match: metrics["qps"] = float(match.group(1)) break # Extract latency metrics latency = {} # Min latency min_match = re.search(r"min:\s+([\d.]+)", summary, re.IGNORECASE) if min_match: latency["min"] = float(min_match.group(1)) # Average latency avg_patterns = [ r"avg:\s+([\d.]+)", r"average latency:\s+([\d.]+)", ] for pattern in avg_patterns: match = re.search(pattern, summary, re.IGNORECASE) if match: latency["avg"] = float(match.group(1)) break # Percentile latencies percentile_patterns = { "p50": [r"50th percentile:\s+([\d.]+)", r"median:\s+([\d.]+)"], "p95": [r"95th percentile:\s+([\d.]+)"], "p99": [r"99th percentile:\s+([\d.]+)"], } for key, patterns in percentile_patterns.items(): for pattern in patterns: match = re.search(pattern, summary, re.IGNORECASE) if match: latency[key] = float(match.group(1)) break # Max latency max_match = re.search(r"max:\s+([\d.]+)", summary, re.IGNORECASE) if max_match: latency["max"] = float(max_match.group(1)) if latency: metrics["latency"] = latency return metrics def parse_tpch_metrics(summary: str) -> Dict: """Parse TPC-H (bench-ch) performance metrics. TPC-H summary format example: ``` tpmC: 30801.4, tpmTotal: 68428.8, efficiency: 239.5% [Summary] Q1 - Count: 5, Sum(ms): 33804.8, Avg(ms): 6760.1 ... QphH: 101.4 ``` This is a hybrid benchmark that includes both OLTP (tpmC) and OLAP (QphH) metrics. """ metrics = {} # Extract tpmC (OLTP transactions per minute) tpmc_match = re.search(r"tpmC:\s*([\d.]+)", summary) if tpmc_match: tpmC = float(tpmc_match.group(1)) metrics["tpmC"] = tpmC metrics["tps"] = tpmC / 60 # Convert TPM to TPS # Extract QphH (OLAP queries per hour) qphh_match = re.search(r"QphH:\s*([\d.]+)", summary) if qphh_match: metrics["QphH"] = float(qphh_match.group(1)) # Extract OLTP latencies from NEW_ORDER summary line (use nested structure for consistency) new_order_match = re.search( r"\[Summary\]\s+NEW_ORDER.*?Avg\(ms\):\s*([\d.]+).*?50th\(ms\):\s*([\d.]+).*?90th\(ms\):\s*([\d.]+).*?95th\(ms\):\s*([\d.]+).*?99th\(ms\):\s*([\d.]+).*?Max\(ms\):\s*([\d.]+)", summary, re.DOTALL, ) if new_order_match: # Use the same nested structure as TPCC and Sysbench for consistency metrics["latency"] = { "avg": float(new_order_match.group(1)), "p50": float(new_order_match.group(2)), "p90": float(new_order_match.group(3)), "p95": float(new_order_match.group(4)), "p99": float(new_order_match.group(5)), "max": float(new_order_match.group(6)), } return metrics def extract_workload_params(node: Dict, task_workload_config: Dict = None) -> Dict: """Extract workload parameters from node and task configuration. Args: node: Node data containing node_name and metadata task_workload_config: Workload configuration from task (optional) Returns: Dictionary of workload parameters """ node_name = node.get("node_name", "") params = {} # Try to get from task workload config first if task_workload_config: # Check if params are under nodes (old structure) if "nodes" in task_workload_config: node_config = task_workload_config["nodes"].get(node_name, {}) if "workload" in node_config and "params" in node_config["workload"]: return node_config["workload"]["params"] # Check if params are at top level (new structure, e.g., bench-ch) if "params" in task_workload_config: config_params = task_workload_config["params"] # Normalize "threads" to "thread" for consistency if "threads" in config_params: params["thread"] = int(config_params["threads"]) elif "thread" in config_params: params["thread"] = int(config_params["thread"]) # Extract other common parameters if "warehouses" in config_params: params["warehouses"] = int(config_params["warehouses"]) if "duration" in config_params: duration_str = config_params["duration"] # Parse duration like "60m" or "2h" if duration_str.endswith("m"): params["duration"] = int(duration_str[:-1]) elif duration_str.endswith("h"): params["duration"] = int(duration_str[:-1]) * 60 return params # Parse from node_name as fallback params.update(parse_params_from_node_name(node_name)) return params def parse_params_from_node_name(node_name: str) -> Dict: """Parse workload parameters from node name. Examples: - tpcc_1k_100th_2hrs -> {"warehouses": 1000, "thread": 100, "duration": 120} - oltp_128th_20m_read -> {"thread": 128, "duration": 20} - sysbench_256th_30m -> {"thread": 256, "duration": 30} """ params = {} # Extract warehouse count (for TPCC) wh_match = re.search(r"(\d+)k", node_name, re.IGNORECASE) if wh_match: params["warehouses"] = int(wh_match.group(1)) * 1000 # Extract thread count th_match = re.search(r"(\d+)th", node_name, re.IGNORECASE) if th_match: params["thread"] = int(th_match.group(1)) # Extract duration # Try hours first dur_match = re.search(r"(\d+)hrs?", node_name, re.IGNORECASE) if dur_match: params["duration"] = int(dur_match.group(1)) * 60 # Convert to minutes else: # Try minutes dur_match = re.search(r"(\d+)m(?:in)?", node_name, re.IGNORECASE) if dur_match: params["duration"] = int(dur_match.group(1)) # Extract workload subtype (for sysbench) sysbench_types = [ "read_write", "point_select", "update_index", "update_non_index", "insert", "read", "write", ] for subtype in sysbench_types: if subtype in node_name.lower(): params["workload_subtype"] = subtype break return params def compute_spu_identifier( tidb_spu: str, tidb_count: int, tikv_spu: str, tikv_count: int, cloud_provider: str, region: str, cluster_version: str, workload_config: dict | None = None, ) -> str: """Compute unique SPU identifier. Args: tidb_spu: TiDB SPU (e.g., "64C256G-V2") tidb_count: Number of TiDB nodes tikv_spu: TiKV SPU tikv_count: Number of TiKV nodes cloud_provider: Cloud provider (e.g., "AWS") region: Region (e.g., "aws-us-west-2") cluster_version: TiDB version (e.g., "v8.5.4") workload_config: Premium workload config with RCU Returns: SPU identifier string """ if is_premium_workload_config(workload_config): rcu = workload_config.get("rcu", {}) return f"{rcu.get('min', '')}_{rcu.get('max', '')}_{cloud_provider}_{region}_{cluster_version}" return ( f"{tidb_spu}x{tidb_count}_" f"{tikv_spu}x{tikv_count}_" f"{cloud_provider}_" f"{region}_" f"{cluster_version}" ) def extract_workload_type_from_template(template: str, node_name: str = "") -> str: """Extract workload type from template and node name. Supports three workload types: - tpcc: TPC-C benchmark - bench-ch (tpch): TPC-H benchmark - sysbench: Sysbench with variants (read, write, mix) Args: template: Workflow template (e.g., "bench-tpcc", "bench-ch", "bench-sysbench") node_name: Node name for additional context Returns: Workload type string (e.g., "tpcc", "bench-ch", "sysbench-read_write") """ # For TPCC if "tpcc" in template.lower(): return "tpcc" # For Sysbench - try to extract subtype from node name if "sysbench" in template.lower(): sysbench_types = { "read_write": "sysbench-read_write", "point_select": "sysbench-point_select", "update_index": "sysbench-update_index", "update_non_index": "sysbench-update_non_index", "insert": "sysbench-insert", } for subtype, workload_type in sysbench_types.items(): if subtype in node_name.lower(): return workload_type return "sysbench" # For TPC-H (bench-ch) if "bench-ch" in template.lower() or "tpch" in template.lower(): return "bench-ch" # Default: use template as-is return template