#!/usr/bin/env python3 """ CLI tool for generating performance reports with flexible filtering. Usage: python -m src.cli.report_cli \ --cloud AWS \ --generation G2 \ --workload tpcc \ --aggregation average \ --output report.md python -m src.cli.report_cli \ --cloud AWS \ --tidb-cores ">=64" \ --workload sysbench \ --format table """ import argparse import sys from rich.console import Console from rich.panel import Panel from rich.table import Table as RichTable from rich.text import Text from src.services.report.aggregation import AggregationStrategy from src.services.report.query_filter import ( FilterDimension, FilterOperator, QueryFilter, ) from src.services.report.report_builder import ( generate_comparison_report, generate_report_from_filter, ) from src.services.report.report_generator import export_to_markdown console = Console() def parse_comparison_operator(value_str: str) -> tuple[FilterOperator, any]: """Parse comparison operators from string like '>=64' or 'AWS'.""" if value_str.startswith(">="): return FilterOperator.GREATER_THAN_OR_EQUAL, int(value_str[2:]) elif value_str.startswith(">"): return FilterOperator.GREATER_THAN, int(value_str[1:]) elif value_str.startswith("<="): return FilterOperator.LESS_THAN_OR_EQUAL, int(value_str[2:]) elif value_str.startswith("<"): return FilterOperator.LESS_THAN, int(value_str[1:]) elif value_str.startswith("!="): return FilterOperator.NOT_EQUALS, value_str[2:] elif "," in value_str: # Multiple values: "AWS,GCP" return FilterOperator.IN, value_str.split(",") else: # Simple equality return FilterOperator.EQUALS, value_str def build_filter_from_args(args) -> QueryFilter: """Build QueryFilter from command line arguments.""" filter_obj = QueryFilter() # Cloud provider if args.cloud: op, val = parse_comparison_operator(args.cloud) filter_obj.add_condition(FilterDimension.CLOUD_PROVIDER, operator=op, value=val) # Region if args.region: op, val = parse_comparison_operator(args.region) filter_obj.add_condition(FilterDimension.REGION, operator=op, value=val) # Machine generation if args.generation: op, val = parse_comparison_operator(args.generation) filter_obj.add_condition( FilterDimension.MACHINE_GENERATION, operator=op, value=val ) # TiDB cores if args.tidb_cores: op, val = parse_comparison_operator(args.tidb_cores) filter_obj.add_condition( FilterDimension.TIDB_TOTAL_CORES, operator=op, value=val ) # TiKV cores if args.tikv_cores: op, val = parse_comparison_operator(args.tikv_cores) filter_obj.add_condition( FilterDimension.TIKV_TOTAL_CORES, operator=op, value=val ) # TiDB node count if args.tidb_count: op, val = parse_comparison_operator(args.tidb_count) filter_obj.add_condition( FilterDimension.TIDB_NODE_COUNT, operator=op, value=val ) # Workload type if args.workload: op, val = parse_comparison_operator(args.workload) filter_obj.add_condition(FilterDimension.WORKLOAD_TYPE, operator=op, value=val) # TiProxy enabled if args.tiproxy is not None: filter_obj.add_condition(FilterDimension.TIPROXY_ENABLED, value=args.tiproxy) # Cluster version if args.version: op, val = parse_comparison_operator(args.version) filter_obj.add_condition( FilterDimension.CLUSTER_VERSION, operator=op, value=val ) return filter_obj def print_report_summary(report): """Print report summary in table format.""" # Cluster info panel config = report.cluster_config info_text = Text() info_text.append("SPU: ", style="bold") info_text.append(f"{report.spu_identifier}\n") info_text.append("TiDB: ", style="bold") info_text.append(f"{config.tidb_spu} × {config.tidb_count} ") info_text.append( f"({config.tidb_total_cores} cores, {config.tidb_total_memory}GB)\n" ) info_text.append("TiKV: ", style="bold") info_text.append(f"{config.tikv_spu} × {config.tikv_count} ") info_text.append( f"({config.tikv_total_cores} cores, {config.tikv_total_memory}GB)\n" ) info_text.append("Cloud: ", style="bold") info_text.append(f"{config.cloud_provider} / {config.region}\n") info_text.append("Version: ", style="bold") info_text.append(f"{config.cluster_version}\n") if config.tiproxy_enabled: info_text.append("TiProxy: ", style="bold green") info_text.append("Enabled\n") console.print( Panel( info_text, title="[bold]Cluster Configuration[/bold]", border_style="blue" ) ) # Workflows for workflow in report.workflows: console.print(f"\n[bold cyan]Workflow:[/bold cyan] {workflow.workflow_type}") console.print( f"[dim]Status: {workflow.status}, Runs: {len(workflow.runs)}[/dim]" ) if not workflow.runs: console.print("[yellow]No runs available[/yellow]") continue # Create performance table table = RichTable(title=f"{workflow.workflow_type} Performance") table.add_column("Thread Count", justify="right", style="cyan") table.add_column("TPS/tpmC", justify="right", style="green") table.add_column("P95 Latency (ms)", justify="right", style="yellow") table.add_column("P99 Latency (ms)", justify="right", style="red") table.add_column("Status", justify="center") for run in workflow.runs: thread_count = run.params.get("thread") or run.params.get("threads") or "-" # Extract metrics tps = run.metrics.get("tps", 0) or run.metrics.get("tpmC", 0) p95 = run.metrics.get("latency", {}).get("p95", 0) p99 = run.metrics.get("latency", {}).get("p99", 0) # Format values tps_str = f"{tps:,.2f}" if tps else "-" p95_str = f"{p95:.2f}" if p95 else "-" p99_str = f"{p99:.2f}" if p99 else "-" status_color = "green" if run.status == "SUCCEEDED" else "red" status_str = f"[{status_color}]{run.status}[/{status_color}]" table.add_row( str(thread_count), tps_str, p95_str, p99_str, status_str, ) console.print(table) def print_comparison_summary(report1, report2): """Print comparison between two reports.""" console.print("\n[bold magenta]Comparison Report[/bold magenta]\n") # Side by side cluster info table = RichTable(title="Cluster Comparison") table.add_column("Metric", style="cyan") table.add_column("Config 1", style="green") table.add_column("Config 2", style="yellow") table.add_row("SPU", report1.spu_identifier, report2.spu_identifier) table.add_row( "TiDB", f"{report1.cluster_config.tidb_spu}×{report1.cluster_config.tidb_count}", f"{report2.cluster_config.tidb_spu}×{report2.cluster_config.tidb_count}", ) table.add_row( "TiKV", f"{report1.cluster_config.tikv_spu}×{report1.cluster_config.tikv_count}", f"{report2.cluster_config.tikv_spu}×{report2.cluster_config.tikv_count}", ) table.add_row( "Generation", report1.cluster_config.tidb_machine_generation or "?", report2.cluster_config.tidb_machine_generation or "?", ) table.add_row( "Cloud/Region", f"{report1.cluster_config.cloud_provider}/{report1.cluster_config.region}", f"{report2.cluster_config.cloud_provider}/{report2.cluster_config.region}", ) console.print(table) # Performance comparison common_workflows = set(w.workflow_type for w in report1.workflows) & set( w.workflow_type for w in report2.workflows ) for workflow_type in common_workflows: wf1 = report1.get_workflow(workflow_type) wf2 = report2.get_workflow(workflow_type) if not wf1 or not wf2 or not wf1.runs or not wf2.runs: continue console.print(f"\n[bold cyan]Workflow:[/bold cyan] {workflow_type}") # Build comparison table perf_table = RichTable(title=f"{workflow_type} Performance Comparison") perf_table.add_column("Thread Count", justify="right", style="cyan") perf_table.add_column("Config 1 TPS", justify="right", style="green") perf_table.add_column("Config 2 TPS", justify="right", style="green") perf_table.add_column("Improvement", justify="right", style="magenta") perf_table.add_column("Config 1 P95", justify="right", style="yellow") perf_table.add_column("Config 2 P95", justify="right", style="yellow") # Match runs by thread count runs1_by_thread = { r.params.get("thread") or r.params.get("threads"): r for r in wf1.runs } runs2_by_thread = { r.params.get("thread") or r.params.get("threads"): r for r in wf2.runs } common_threads = set(runs1_by_thread.keys()) & set(runs2_by_thread.keys()) for thread_count in sorted(common_threads): run1 = runs1_by_thread[thread_count] run2 = runs2_by_thread[thread_count] tps1 = run1.metrics.get("tps", 0) or run1.metrics.get("tpmC", 0) tps2 = run2.metrics.get("tps", 0) or run2.metrics.get("tpmC", 0) p95_1 = run1.metrics.get("latency", {}).get("p95", 0) p95_2 = run2.metrics.get("latency", {}).get("p95", 0) # Calculate improvement improvement = 0 if tps1 and tps2: improvement = ((tps2 - tps1) / tps1) * 100 improvement_str = f"{improvement:+.1f}%" if improvement > 0: improvement_str = f"[green]{improvement_str}[/green]" elif improvement < 0: improvement_str = f"[red]{improvement_str}[/red]" perf_table.add_row( str(thread_count), f"{tps1:,.2f}" if tps1 else "-", f"{tps2:,.2f}" if tps2 else "-", improvement_str, f"{p95_1:.2f}" if p95_1 else "-", f"{p95_2:.2f}" if p95_2 else "-", ) console.print(perf_table) def main(): parser = argparse.ArgumentParser( description="Generate performance reports with flexible filtering", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: # Single report for AWS G2 machines running TPCC %(prog)s --cloud AWS --generation G2 --workload tpcc # Compare G2 vs G4 performance %(prog)s --cloud AWS --generation G2 --workload tpcc --compare-generation G4 # Filter by TiDB cores (>= 64) %(prog)s --cloud AWS --tidb-cores ">=64" --workload sysbench # Export to markdown %(prog)s --cloud AWS --generation G2 --output report.md """, ) # Filter arguments parser.add_argument( "--cloud", help="Cloud provider (AWS, GCP) or multiple: AWS,GCP" ) parser.add_argument("--region", help="Cloud region (us-west-2, etc.)") parser.add_argument( "--generation", help="Machine generation (G2=Graviton2, G4=Graviton4) or multiple: G2,G4", ) parser.add_argument( "--tidb-cores", help="TiDB total cores (supports >=64, >32, =64, etc.)", ) parser.add_argument( "--tikv-cores", help="TiKV total cores (supports >=64, >32, =64, etc.)", ) parser.add_argument("--tidb-count", help="TiDB node count") parser.add_argument("--workload", help="Workload type (tpcc, sysbench, etc.)") parser.add_argument( "--tiproxy", action="store_true", default=None, help="Filter for TiProxy enabled", ) parser.add_argument( "--no-tiproxy", dest="tiproxy", action="store_false", help="Filter for TiProxy disabled", ) parser.add_argument("--version", help="Cluster version (v8.5.4, etc.)") # Aggregation strategy parser.add_argument( "--aggregation", choices=["all", "average", "best", "latest", "median"], default="latest", help="Aggregation strategy for multiple runs (default: latest)", ) # Comparison mode parser.add_argument( "--compare-generation", help="Generate comparison report with different generation (G2/G4)", ) parser.add_argument( "--compare-cloud", help="Generate comparison report with different cloud provider", ) # Output options parser.add_argument( "--output", "-o", help="Output file path (markdown format)", ) parser.add_argument( "--format", choices=["table", "markdown"], default="table", help="Output format (default: table)", ) args = parser.parse_args() # Build aggregation strategy agg_strategy = AggregationStrategy(args.aggregation) # Build filter filter1 = build_filter_from_args(args) if not filter1.conditions: console.print("[red]Error: At least one filter condition is required[/red]") parser.print_help() sys.exit(1) console.print(f"\n[bold]Filter:[/bold] {filter1.to_description()}") console.print(f"[bold]Aggregation:[/bold] {agg_strategy.value}\n") # Comparison mode if args.compare_generation or args.compare_cloud: # Build second filter filter2_args = argparse.Namespace(**vars(args)) if args.compare_generation: filter2_args.generation = args.compare_generation if args.compare_cloud: filter2_args.cloud = args.compare_cloud filter2 = build_filter_from_args(filter2_args) console.print("[bold]Generating comparison report...[/bold]") report1, report2 = generate_comparison_report(filter1, filter2, agg_strategy) if not report1: console.print("[red]Error: No data found for first filter[/red]") sys.exit(1) if not report2: console.print("[red]Error: No data found for second filter[/red]") sys.exit(1) # Print comparison print_comparison_summary(report1, report2) else: # Single report mode console.print("[bold]Generating report...[/bold]") report = generate_report_from_filter(filter1, agg_strategy) if not report: console.print("[red]Error: No data found matching the filter[/red]") sys.exit(1) # Print report if args.output: # When output file is specified, always generate markdown markdown = export_to_markdown(report) with open(args.output, "w") as f: f.write(markdown) console.print(f"\n[green]Report saved to {args.output}[/green]") elif args.format == "markdown": # Markdown to console markdown = export_to_markdown(report) console.print(markdown) else: # Default: table format to console print_report_summary(report) console.print("\n[green]✓ Report generation completed[/green]") if __name__ == "__main__": main()