defmodule Mix.Tasks.Firebird.Target.Report do @moduledoc """ Generate a compilation report for the WASM target. Compiles all WASM sources and produces a detailed report with per-module statistics, aggregate totals, and timing information. ## Usage # Generate report to stdout mix firebird.target.report # Save report as JSON mix firebird.target.report --json --output build_report.json # Compare with previous report mix firebird.target.report --compare previous_report.json # Quick summary only mix firebird.target.report --summary ## Output The report includes: - Per-module WAT/WASM sizes - Export counts - Compression ratio - Compilation timing - Configuration used """ use Mix.Task @shortdoc "Generate a WASM compilation report" @switches [ output: :string, json: :boolean, compare: :string, summary: :boolean, dirs: :string, files: :string, optimize: :boolean, tco: :boolean, inline: :boolean, wat_only: :boolean ] @aliases [ o: :output, d: :dirs, f: :files ] @impl Mix.Task def run(args) do {opts, _rest, _invalid} = OptionParser.parse(args, switches: @switches, aliases: @aliases) json_mode = Keyword.get(opts, :json, false) summary_only = Keyword.get(opts, :summary, false) config = Mix.Tasks.Firebird.Target.Compile.build_config(opts) # Discover and compile sources = Firebird.Target.discover_sources(config) if Enum.empty?(sources) do Mix.shell().info("No compilable sources found.") :ok else start = System.monotonic_time(:millisecond) {:ok, results} = Firebird.Target.compile(config) elapsed = System.monotonic_time(:millisecond) - start # Handle partial failure results = case results do list when is_list(list) -> list _ -> [] end report = Firebird.Target.Report.generate(results, config, elapsed) # Output cond do json_mode -> json = Firebird.Target.Report.to_json(report) if output = Keyword.get(opts, :output) do File.write!(output, json) Mix.shell().info("📋 Report saved to #{output}") else Mix.shell().info(json) end summary_only -> print_summary(report) true -> Mix.shell().info(Firebird.Target.Report.format(report)) end # Compare with previous if requested if compare_path = Keyword.get(opts, :compare) do compare_with(report, compare_path) end :ok end end defp print_summary(report) do t = report.totals Mix.shell().info("📊 WASM Compilation Summary") Mix.shell().info(" Modules: #{t.module_count}") Mix.shell().info(" WAT: #{format_bytes(t.total_wat_bytes)}") Mix.shell().info(" WASM: #{format_bytes(t.total_wasm_bytes)}") if t.compression_ratio do Mix.shell().info(" Ratio: #{t.compression_ratio}%") end Mix.shell().info(" Time: #{report.elapsed_ms}ms") end defp compare_with(current, path) do case File.read(path) do {:ok, json} -> case Jason.decode(json) do {:ok, prev_data} -> prev_report = normalize_report(prev_data) diff = Firebird.Target.Report.diff(prev_report, current) Mix.shell().info("") Mix.shell().info("📊 Comparison with #{path}:") Mix.shell().info(" Modules: #{format_change(diff.module_count_change)}") Mix.shell().info( " WAT: #{format_bytes_change(diff.wat_bytes_change, diff.wat_percent_change)}" ) Mix.shell().info( " WASM: #{format_bytes_change(diff.wasm_bytes_change, diff.wasm_percent_change)}" ) Mix.shell().info(" Time: #{format_time_change(diff.time_change_ms)}") _ -> Mix.shell().error("Could not parse #{path}") end {:error, _} -> Mix.shell().error("Could not read #{path}") end end defp normalize_report(data) when is_map(data) do %{ timestamp: data["timestamp"] || "", elapsed_ms: data["elapsed_ms"] || 0, modules: data["modules"] || [], totals: %{ module_count: get_in(data, ["totals", "module_count"]) || 0, total_wat_bytes: get_in(data, ["totals", "total_wat_bytes"]) || 0, total_wasm_bytes: get_in(data, ["totals", "total_wasm_bytes"]) || 0, compression_ratio: get_in(data, ["totals", "compression_ratio"]) }, config: data["config"] || %{} } end defp format_change(0), do: "no change" defp format_change(n) when n > 0, do: "+#{n}" defp format_change(n), do: "#{n}" defp format_bytes_change(bytes, pct) do sign = if bytes >= 0, do: "+", else: "" pct_str = if pct, do: " (#{sign}#{pct}%)", else: "" "#{sign}#{format_bytes(abs(bytes))}#{pct_str}" end defp format_time_change(0), do: "no change" defp format_time_change(ms) when ms > 0, do: "+#{ms}ms (slower)" defp format_time_change(ms), do: "#{ms}ms (faster)" defp format_bytes(0), do: "0 B" defp format_bytes(bytes) when bytes < 1024, do: "#{bytes} B" defp format_bytes(bytes) when bytes < 1_048_576, do: "#{Float.round(bytes / 1024, 1)} KB" defp format_bytes(bytes), do: "#{Float.round(bytes / 1_048_576, 1)} MB" end