defmodule Mix.Tasks.Raxol.Bench do @moduledoc """ Enhanced benchmarking task for Raxol terminal emulator. Provides comprehensive performance benchmarking with multiple output formats, regression detection, and dashboard generation. ## Usage mix raxol.bench # Run all benchmarks mix raxol.bench parser # Run parser benchmarks only mix raxol.bench terminal # Run terminal component benchmarks mix raxol.bench rendering # Run rendering benchmarks only mix raxol.bench memory # Run memory benchmarks only mix raxol.bench dashboard # Generate dashboard from latest results ## Options * `--quick` - Quick benchmark run with reduced time * `--compare` - Compare with previous benchmark results * `--dashboard` - Generate full performance dashboard with charts * `--regression` - Check for performance regressions (5% threshold) * `--help` - Show detailed help ## Examples # Quick performance check mix raxol.bench --quick # Full benchmarks with regression detection mix raxol.bench --regression # Parser benchmarks with dashboard generation mix raxol.bench parser --dashboard # Compare current run with previous results mix raxol.bench --compare ## Output Results are saved to `bench/output/enhanced/` with: - HTML reports for interactive viewing - JSON data for programmatic analysis - Performance dashboard with charts - Regression analysis reports - Baseline metrics for future comparison """ use Mix.Task @shortdoc "Run enhanced performance benchmarks" @switches [ quick: :boolean, compare: :boolean, dashboard: :boolean, regression: :boolean, help: :boolean ] # Performance regression threshold (5%) @regression_threshold 0.05 def run(args) do {opts, args, _} = OptionParser.parse(args, switches: @switches) case {opts[:help], args} do {true, _} -> print_help() {false, benchmark_args} -> Mix.Task.run("app.start") execute_benchmark(benchmark_args, opts) end end defp execute_benchmark(args, opts) do case args do [] -> run_all_benchmarks(opts) ["parser"] -> run_parser_only(opts) ["terminal"] -> run_terminal_only(opts) ["rendering"] -> run_rendering_only(opts) ["memory"] -> run_memory_only(opts) ["dashboard"] -> run_dashboard_only(opts) [benchmark] -> Mix.shell().error("Unknown benchmark: #{benchmark}") print_help() end end defp run_all_benchmarks(opts) do Mix.shell().info("Running Raxol Performance Benchmarks...") if opts[:quick] do run_quick_benchmarks() else run_comprehensive_benchmarks(opts) end end defp run_quick_benchmarks do Mix.shell().info("Quick benchmark mode (reduced time)") # Run a subset of benchmarks with reduced time benchmark_config = [ time: 1, memory_time: 0.5, warmup: 0.2, formatters: [Benchee.Formatters.Console] ] _ = run_parser_benchmarks(benchmark_config, quick: true) _ = run_terminal_benchmarks(benchmark_config, quick: true) end defp run_comprehensive_benchmarks(opts) do Mix.shell().info("Running comprehensive benchmarks...") # Create output directories File.mkdir_p!("bench/output/enhanced") File.mkdir_p!("bench/output/enhanced/json") File.mkdir_p!("bench/output/enhanced/html") timestamp = DateTime.utc_now() |> DateTime.to_iso8601() # Run all benchmark suites results = %{} results = Map.put( results, :parser, run_parser_benchmarks(standard_config(timestamp)) ) results = Map.put( results, :terminal, run_terminal_benchmarks(standard_config(timestamp)) ) results = Map.put( results, :rendering, run_rendering_benchmarks(standard_config(timestamp)) ) results = Map.put( results, :memory, run_memory_benchmarks(standard_config(timestamp)) ) results = Map.put( results, :concurrent, run_concurrent_benchmarks(standard_config(timestamp)) ) if opts[:regression] do check_for_regressions(results) end if opts[:dashboard] do generate_enhanced_dashboard(results, timestamp) end if opts[:compare] do run_comparison_analysis(results, timestamp) end print_results_summary(results, timestamp) end defp run_parser_only(opts) do Mix.shell().info("Running Parser Benchmarks Only...") config = if opts[:quick], do: quick_config(), else: standard_config() run_parser_benchmarks(config) end defp run_terminal_only(opts) do Mix.shell().info("Running Terminal Component Benchmarks Only...") config = if opts[:quick], do: quick_config(), else: standard_config() run_terminal_benchmarks(config) end defp run_rendering_only(opts) do Mix.shell().info("Running Rendering Benchmarks Only...") config = if opts[:quick], do: quick_config(), else: standard_config() run_rendering_benchmarks(config) end defp run_memory_only(opts) do Mix.shell().info("Running Memory Benchmarks Only...") config = if opts[:quick], do: quick_config(), else: standard_config() run_memory_benchmarks(config) end defp run_dashboard_only(_opts) do Mix.shell().info("Generating Performance Dashboard...") # Load latest benchmark results and generate dashboard timestamp = DateTime.utc_now() |> DateTime.to_iso8601() results = load_latest_results() || %{} generate_enhanced_dashboard(results, timestamp) Mix.shell().info( "Dashboard generated at bench/output/enhanced/dashboard.html" ) end defp run_parser_benchmarks(config, opts \\ []) do # Proper aliases for Raxol modules alias Raxol.Terminal.ANSI.StateMachine alias Raxol.Terminal.ANSI.Utils.AnsiParser alias Raxol.Terminal.Emulator _emulator = Emulator.new(80, 24) scenarios = if opts[:quick] do %{ "plain_text" => "Hello, World!", "ansi_basic" => "\e[31mRed Text\e[0m", "ansi_complex" => "\e[1;4;31mBold Red Underlined\e[0m" } else get_full_parser_scenarios() end jobs = Enum.into(scenarios, %{}, fn {name, content} -> {name, fn -> state_machine = StateMachine.new() AnsiParser.parse(state_machine, content) end} end) Benchee.run(jobs, config) end defp run_terminal_benchmarks(config, opts \\ []) do jobs = if opts[:quick], do: terminal_quick_jobs(), else: terminal_comprehensive_jobs() Benchee.run(jobs, config) end defp run_rendering_benchmarks(config) do alias Raxol.UI.Rendering.Pipeline alias Raxol.UI.Rendering.RenderBatcher {small_buffer, medium_buffer, large_buffer} = create_test_buffers() jobs = %{ "render_small_buffer" => fn -> Pipeline.render(small_buffer) end, "render_medium_buffer" => fn -> Pipeline.render(medium_buffer) end, "render_large_buffer" => fn -> Pipeline.render(large_buffer) end, "render_with_damage_tracking" => fn -> RenderBatcher.batch_render(medium_buffer, changed_regions: [{0, 0, 10, 10}] ) end, "render_full_redraw" => fn -> RenderBatcher.batch_render(large_buffer, full_redraw: true) end } Benchee.run(jobs, config) end defp run_memory_benchmarks(config) do alias Raxol.Terminal.ANSI.StateMachine alias Raxol.Terminal.ANSI.Utils.AnsiParser alias Raxol.Terminal.Cursor alias Raxol.Terminal.Emulator alias Raxol.Terminal.ScreenBufferAdapter, as: ScreenBuffer jobs = %{ "memory_emulator_80x24" => fn -> Emulator.new(80, 24) end, "memory_emulator_200x50" => fn -> Emulator.new(200, 50) end, "memory_parser_session" => fn -> emulator = Emulator.new(80, 24) state_machine = StateMachine.new() content = generate_test_content() _ = AnsiParser.parse(state_machine, content) {emulator, state_machine} end, "memory_buffer_operations" => fn -> buffer = ScreenBuffer.new(100, 30) _cursor = Cursor.new() buffer = Enum.reduce(1..50, buffer, fn i, acc -> ScreenBuffer.write_string( acc, 0, rem(i, 30), "Test line #{i}" ) end) buffer end, "memory_large_buffer_allocation" => fn -> ScreenBuffer.new(500, 500) end, "memory_plugin_system" => fn -> alias Raxol.Plugins.Manager # Create a new manager instance {:ok, manager} = Manager.new() Manager.list_plugins(manager) end } Benchee.run(jobs, config) end defp run_concurrent_benchmarks(config) do jobs = %{ "concurrent_emulator_creation" => &bench_emulator_creation/0, "concurrent_buffer_writes" => &bench_buffer_writes/0, "concurrent_event_dispatch" => &bench_event_dispatch/0, "concurrent_plugin_operations" => &bench_plugin_operations/0 } Benchee.run(jobs, config) end defp bench_emulator_creation do alias Raxol.Terminal.Emulator 1..10 |> Enum.map(fn _ -> Task.async(fn -> Emulator.new(80, 24) end) end) |> Task.await_many() end defp bench_buffer_writes do alias Raxol.Terminal.ScreenBufferAdapter, as: ScreenBuffer buffer = ScreenBuffer.new(80, 24) 1..10 |> Enum.map(fn i -> Task.async(fn -> ScreenBuffer.write_char(buffer, i, i, "X") end) end) |> Task.await_many() end defp bench_event_dispatch do alias Raxol.Core.Events.EventManager # Handle already_started case in CI/test environments case EventManager.start_link(name: EventManager) do {:ok, _pid} -> :ok {:error, {:already_started, _pid}} -> :ok end 1..20 |> Enum.map(fn i -> Task.async(fn -> EventManager.dispatch(:test_event, %{id: i}) end) end) |> Task.await_many() end defp bench_plugin_operations do alias Raxol.Plugins.Manager # Create a new manager instance {:ok, manager} = Manager.new() 1..5 |> Enum.map(fn _ -> Task.async(fn -> _plugins = Manager.list_plugins(manager) Manager.get_plugin_state(manager, "test_plugin") end) end) |> Task.await_many() end defp standard_config(timestamp \\ nil) do ts = timestamp || DateTime.utc_now() |> DateTime.to_iso8601() [ time: 5, memory_time: 2, warmup: 1, formatters: [ Benchee.Formatters.Console, {Benchee.Formatters.JSON, file: "bench/output/enhanced/json/benchmark_#{ts}.json"}, {Benchee.Formatters.HTML, file: "bench/output/enhanced/html/benchmark_#{ts}.html"} ], save: [path: "bench/output/enhanced/benchee_#{ts}.benchee"], load: "bench/output/enhanced/*.benchee" ] end defp quick_config do [ time: 1, memory_time: 0.5, warmup: 0.2, formatters: [Benchee.Formatters.Console] ] end defp get_full_parser_scenarios do %{ "plain_text_short" => "Hello, World!", "plain_text_long" => String.duplicate("Lorem ipsum dolor sit amet. ", 50), "ansi_basic_color" => "\e[31mRed Text\e[0m", "ansi_complex_sgr" => "\e[1;4;31;48;5;196mComplex Formatting\e[0m", "ansi_cursor_movement" => "\e[2J\e[H\e[10;20H\e[K", "ansi_scroll_region" => "\e[5;20r\e[?25l\e[33mText\e[?25h\e[0;0r", "ansi_character_sets" => "\e(B\e)0\e*B\e+B", "ansi_device_status" => "\e[6n\e[?6c\e[0c", "mixed_realistic" => generate_realistic_terminal_content(), "rapid_colors" => generate_rapid_color_sequence(), "heavy_escape_sequences" => generate_heavy_escape_content() } end defp generate_realistic_terminal_content do """ \e[2J\e[H\e[1;37;44m Terminal Session \e[0m \e[2;1H\e[32mStatus: \e[0mConnected \e[3;1H\e[33mProgress: \e[0m\e[32m████████\e[37m░░\e[0m 80% \e[5;1H\e[1mOptions:\e[0m \e[6;3H\e[36m1.\e[0m Start process \e[7;3H\e[36m2.\e[0m View logs \e[8;3H\e[36m3.\e[0m Exit \e[10;1H\e[90m────────────────────────────────\e[0m \e[11;1HType your choice: \e[5m_\e[25m """ end defp generate_rapid_color_sequence do Enum.map_join(1..100, " ", fn i -> "\e[#{rem(i, 8) + 30}mC#{i}\e[0m" end) end defp generate_heavy_escape_content do sequences = [ # Clear screen "\e[2J", # Home "\e[H", # Alternative buffer "\e[?1049h", # Hide cursor "\e[?25l", # Set scroll region "\e[1;1r", # Reset "\e[m", # 256 color "\e[38;5;196m", # RGB color "\e[48;2;255;0;0m", # Erase line "\e[K", # Erase entire line "\e[2K", # Erase below "\e[J", # Erase above "\e[1J", # Show cursor "\e[?25h", # Normal buffer "\e[?1049l" ] Enum.join(sequences, "Test ") end defp generate_test_content do """ \e[2J\e[H\e[31mTest content with colors\e[0m \e[2;1HSecond line with \e[1mbold\e[0m text \e[3;1H\e[32mGreen text\e[0m and \e[34mblue text\e[0m \e[4;1H\e[38;5;196mExtended color\e[0m \e[5;1H\e[48;2;255;255;0mRGB background\e[0m """ end defp check_for_regressions(results) do Mix.shell().info("\nChecking for performance regressions...") baseline = load_baseline_metrics() if baseline do regressions = detect_regressions(results, baseline) handle_regression_results(regressions) else save_baseline_metrics(results) Mix.shell().info("Baseline metrics saved for future comparisons") end end defp detect_regressions(_current_results, _baseline) do # Compare current results with baseline and detect regressions # Returns list of {benchmark_name, {current_time, baseline_time, degradation_percent}} # Simplified for now [] end defp load_baseline_metrics do baseline_file = "bench/output/enhanced/baseline.json" case File.exists?(baseline_file) do true -> load_and_parse_json_file(baseline_file) false -> nil end end defp save_baseline_metrics(results) do baseline_file = "bench/output/enhanced/baseline.json" File.mkdir_p!(Path.dirname(baseline_file)) # Convert results to baseline format baseline_data = %{ timestamp: DateTime.utc_now() |> DateTime.to_iso8601(), metrics: results } case Jason.encode(baseline_data) do {:ok, json} -> File.write!(baseline_file, json) _ -> Mix.shell().error("Failed to save baseline metrics") end end defp generate_enhanced_dashboard(_results, timestamp) do dashboard_content = """