Packages

Performance profiling and optimization toolkit for Ash Framework applications. Analyze DSL complexity, identify compilation bottlenecks, and get actionable optimization recommendations.

Current section

Files

Jump to
ash_profiler lib mix tasks debug_compilation.ex
Raw

lib/mix/tasks/debug_compilation.ex

defmodule Mix.Tasks.DebugCompilation do
use Mix.Task
@shortdoc "Debug Ash DSL compilation performance"
@moduledoc """
Comprehensive analysis of Ash DSL compilation performance issues.
Usage:
mix debug_compilation
mix debug_compilation --container-mode
mix debug_compilation --profile-only
mix debug_compilation --benchmark
"""
def run(args) do
{opts, _, _} =
OptionParser.parse(args,
switches: [container_mode: :boolean, profile_only: :boolean, benchmark: :boolean]
)
IO.puts("🔍 Starting Ash DSL Compilation Debug")
IO.puts("Environment: #{if opts[:container_mode], do: "Container", else: "Local"}")
IO.puts("Time: #{DateTime.utc_now()}")
# Ensure application is loaded but not started
Mix.Task.run("loadpaths")
# Load app modules for introspection
try do
Mix.Task.run("compile", ["--no-deps-check"])
rescue
error ->
IO.puts("⚠️ Compilation failed during setup: #{inspect(error)}")
IO.puts("Continuing with available modules...")
end
# Run container analysis if requested
if opts[:container_mode] do
AshProfiler.ContainerProfiler.analyze_container_environment()
# Run performance benchmark
if opts[:benchmark] do
score = AshProfiler.ContainerProfiler.benchmark_compilation_environment()
if score > 2.0 do
IO.puts(
"\n🚨 PERFORMANCE ALERT: Container environment is too slow for efficient compilation"
)
end
end
end
# Run DSL profiling unless profile-only is disabled
unless opts[:profile_only] == false do
AshProfiler.DSLProfiler.profile_dsl_compilation()
end
# Run targeted compilation tests
run_compilation_tests()
# Analyze specific Ash patterns
analyze_ash_patterns()
# Generate recommendations
generate_recommendations(opts)
IO.puts("✅ Debug analysis complete")
end
defp run_compilation_tests do
IO.puts("\n=== Running Compilation Tests ===")
# Test clean compilation
IO.puts("\nTesting clean compilation...")
# Clear build but preserve deps
File.rm_rf(Mix.Project.build_path())
{compile_time, {output, _exit_code}} =
:timer.tc(fn ->
System.cmd("mix", ["compile", "--force", "--all-warnings"],
stderr_to_stdout: true,
cd: File.cwd!()
)
end)
compile_seconds = compile_time / 1_000_000
IO.puts("Clean compilation time: #{Float.round(compile_seconds, 2)} seconds")
if compile_seconds > 300 do
IO.puts("⚠️ SLOW COMPILATION DETECTED (> 5 minutes)")
# Look for specific slow patterns in output
if String.contains?(output, "Compiling") do
slow_files = extract_slow_compiling_files(output)
if length(slow_files) > 0 do
IO.puts("Slowest files to compile:")
Enum.each(slow_files, &IO.puts(" - #{&1}"))
end
end
end
# Test incremental compilation
IO.puts("\nTesting incremental compilation...")
{incremental_time, _} =
:timer.tc(fn ->
System.cmd("mix", ["compile"], stderr_to_stdout: true, cd: File.cwd!())
end)
incremental_seconds = incremental_time / 1_000_000
IO.puts("Incremental compilation time: #{Float.round(incremental_seconds, 2)} seconds")
# Analyze compilation ratio
if incremental_seconds > 0 do
ratio = compile_seconds / incremental_seconds
IO.puts("Clean/Incremental ratio: #{Float.round(ratio, 1)}x")
if ratio < 5 do
IO.puts("⚠️ Poor incremental compilation performance")
end
end
end
defp extract_slow_compiling_files(output) do
# Extract file names that might be causing slowdowns
output
|> String.split("\n")
|> Enum.filter(&String.contains?(&1, "Compiling"))
|> Enum.map(fn line ->
# Extract file names from compilation output
case Regex.run(~r/Compiling\s+.*\s+\((.*)\)/, line) do
[_, file] ->
file
_ ->
case Regex.run(~r/==>\s+Compiling\s+(.*)/, line) do
[_, app] -> app
_ -> line
end
end
end)
# Top 5
|> Enum.take(5)
end
defp analyze_ash_patterns do
IO.puts("\n=== Analyzing Ash-Specific Patterns ===")
# Check for common performance issues
check_for_common_ash_issues()
# Analyze resource interdependencies
analyze_resource_dependencies()
# Check for expensive validations
check_expensive_validations()
end
defp check_for_common_ash_issues do
IO.puts("\nChecking for known Ash performance issues...")
issues_found =
[]
|> check_state_machine_resources()
|> check_complex_policies()
|> check_relationship_heavy_resources()
|> check_circular_resource_deps()
|> check_expensive_computed_attributes()
if length(issues_found) == 0 do
IO.puts("✅ No obvious Ash performance issues found")
end
issues_found
end
defp check_state_machine_resources(issues_found) do
if has_state_machine_resources?() do
IO.puts("⚠️ AshStateMachine detected - known to slow compilation significantly")
["state_machine" | issues_found]
else
issues_found
end
end
defp check_complex_policies(issues_found) do
if has_complex_policies?() do
IO.puts("⚠️ Complex authorization policies detected")
["complex_policies" | issues_found]
else
issues_found
end
end
defp check_relationship_heavy_resources(issues_found) do
heavy_resources = find_relationship_heavy_resources()
if length(heavy_resources) > 0 do
IO.puts("⚠️ Resources with many relationships detected:")
Enum.each(heavy_resources, fn {file, count} ->
IO.puts(" #{file}: #{count} relationships")
end)
["many_relationships" | issues_found]
else
issues_found
end
end
defp check_circular_resource_deps(issues_found) do
if has_circular_resource_deps?() do
IO.puts("⚠️ Potential circular resource dependencies detected")
["circular_deps" | issues_found]
else
issues_found
end
end
defp check_expensive_computed_attributes(issues_found) do
if has_expensive_computed_attributes?() do
IO.puts("⚠️ Expensive computed attributes detected")
["expensive_computed" | issues_found]
else
issues_found
end
end
defp analyze_resource_dependencies do
IO.puts("\nAnalyzing resource dependency graph...")
# Find all resource files
resource_files = find_ash_resource_files()
# Build dependency map
deps = build_resource_dependency_map(resource_files)
# Look for complex dependency patterns
complex_deps =
Enum.filter(deps, fn {_resource, resource_deps} ->
length(resource_deps) > 5
end)
if length(complex_deps) > 0 do
IO.puts("Resources with many dependencies:")
Enum.each(complex_deps, fn {resource, resource_deps} ->
IO.puts(" #{resource}: depends on #{length(resource_deps)} other resources")
end)
end
# Check for mutual dependencies
mutual_deps = find_mutual_dependencies(deps)
if length(mutual_deps) > 0 do
IO.puts("⚠️ Mutual dependencies found:")
Enum.each(mutual_deps, fn {a, b} ->
IO.puts(" #{a}#{b}")
end)
end
end
defp check_expensive_validations do
IO.puts("\nChecking for expensive validations...")
# Look for validations that might be slow
expensive_patterns = [
~r/validate.*Enum\.all\?/,
~r/validate.*Enum\.any\?/,
~r/validate.*Repo\.get/,
~r/validate.*Repo\.exists\?/,
~r/change.*Repo\./,
~r/validate.*HTTPoison/,
~r/validate.*Req\./
]
Path.wildcard("lib/**/*.ex")
|> Enum.each(fn file ->
content = File.read!(file)
for pattern <- expensive_patterns do
if Regex.match?(pattern, content) do
matches = Regex.scan(pattern, content) |> length()
if matches > 0 do
IO.puts("⚠️ #{file}: #{matches} potentially expensive validations")
end
end
end
end)
end
defp generate_recommendations(opts) do
IO.puts("\n=== 🔧 Performance Recommendations ===")
container_mode = opts[:container_mode]
if container_mode do
IO.puts("\n📦 Container-Specific Optimizations:")
IO.puts(" • Increase container memory limit (minimum 4GB for Ash projects)")
IO.puts(" • Use multi-stage Docker builds with compilation cache")
IO.puts(" • Set ELIXIR_ERL_OPTIONS=\"+sbwt none +sbwtdcpu none +sbwtdio none\"")
IO.puts(" • Consider using BuildKit with cache mounts")
IO.puts(" • Use faster storage (avoid shared volumes for compilation)")
end
IO.puts("\n⚡ Ash DSL Optimizations:")
IO.puts(" • Split large resources into smaller, focused resources")
IO.puts(" • Move complex computed attributes to separate modules")
IO.puts(" • Simplify authorization policy expressions")
IO.puts(" • Reduce relationship count per resource (< 10 recommended)")
IO.puts(" • Use lazy-loaded relationships where possible")
IO.puts("\n🚀 Compilation Optimizations:")
IO.puts(" • Set ASH_DISABLE_COMPILE_DEPENDENCY_TRACKING=true in dev")
IO.puts(" • Use mix compile.ash --no-deps-check for faster rebuilds")
IO.puts(" • Consider splitting domains by compilation independence")
IO.puts(" • Cache _build directory in CI/CD pipelines")
IO.puts("\n🛠️ Development Workflow:")
IO.puts(" • Use mix compile.ash --watch for development")
IO.puts(" • Create simplified resource definitions for test env")
IO.puts(" • Use --parallel flag: mix compile --parallel")
# Check current Mix environment
if Mix.env() == :prod do
IO.puts("\n🏭 Production Build Optimizations:")
IO.puts(" • MIX_ENV=prod mix compile --no-debug-info")
IO.puts(" • Use mix release for optimized production builds")
IO.puts(" • Enable compiler optimizations with --optimize")
end
# Environment-specific suggestions
if container_mode do
suggest_container_dockerfile()
else
suggest_local_optimizations()
end
end
defp suggest_container_dockerfile do
IO.puts("\n🐳 Suggested Dockerfile optimizations:")
IO.puts("""
# Multi-stage build with dependency caching
FROM elixir:1.15-alpine AS deps
WORKDIR /app
COPY mix.exs mix.lock ./
RUN mix deps.get --only=prod
FROM elixir:1.15-alpine AS build
WORKDIR /app
ENV MIX_ENV=prod
ENV ELIXIR_ERL_OPTIONS="+sbwt none +sbwtdcpu none +sbwtdio none"
COPY --from=deps /app/deps deps/
COPY . .
RUN mix compile --no-deps-check --force
""")
end
defp suggest_local_optimizations do
IO.puts("\n💻 Local development optimizations:")
IO.puts(
" • Add to .bashrc: export ELIXIR_ERL_OPTIONS=\"+sbwt none +sbwtdcpu none +sbwtdio none\""
)
IO.puts(" • Use: mix compile --parallel")
IO.puts(" • Consider using: mix compile.ash --no-compile-dependencies")
IO.puts(" • For VSCode: disable Elixir LS on_type_formatting")
end
# Helper functions for analysis
defp has_state_machine_resources?() do
grep_pattern("AshStateMachine")
end
defp has_complex_policies?() do
grep_pattern("authorize_if") && (grep_pattern(" and ") || grep_pattern(" or "))
end
defp find_relationship_heavy_resources() do
Path.wildcard("lib/**/*.ex")
|> Enum.map(fn file ->
content = File.read!(file)
relationship_count = count_relationships(content)
{file, relationship_count}
end)
|> Enum.filter(fn {_file, count} -> count > 8 end)
|> Enum.sort_by(fn {_file, count} -> -count end)
end
defp has_circular_resource_deps?() do
# Simple heuristic: look for resources that import each other
resource_files = find_ash_resource_files()
Enum.any?(resource_files, fn file ->
content = File.read!(file)
other_resources = resource_files -- [file]
Enum.count(other_resources, fn other_file ->
resource_name = extract_resource_name(other_file)
String.contains?(content, resource_name)
end) > 3
end)
end
defp has_expensive_computed_attributes?() do
grep_pattern("calculate") && (grep_pattern("Repo.") || grep_pattern("Enum."))
end
defp find_ash_resource_files() do
Path.wildcard("lib/**/*.ex")
|> Enum.filter(fn file ->
content = File.read!(file)
String.contains?(content, "use Ash.Resource")
end)
end
defp build_resource_dependency_map(resource_files) do
Enum.map(resource_files, fn file ->
content = File.read!(file)
resource_name = extract_resource_name(file)
# Find dependencies by looking for other resource references
deps =
resource_files
|> Enum.reject(&(&1 == file))
|> Enum.filter(fn other_file ->
other_resource = extract_resource_name(other_file)
String.contains?(content, other_resource)
end)
|> Enum.map(&extract_resource_name/1)
{resource_name, deps}
end)
end
defp find_mutual_dependencies(deps) do
# Find pairs where A depends on B and B depends on A
for {resource_a, deps_a} <- deps,
{resource_b, deps_b} <- deps,
# Avoid duplicates
resource_a < resource_b,
resource_b in deps_a,
resource_a in deps_b do
{resource_a, resource_b}
end
end
defp count_relationships(content) do
relationship_patterns = [~r/belongs_to/, ~r/has_many/, ~r/has_one/, ~r/many_to_many/]
Enum.sum(
Enum.map(relationship_patterns, fn pattern ->
length(Regex.scan(pattern, content))
end)
)
end
defp extract_resource_name(file_path) do
file_path
|> Path.basename(".ex")
|> String.split("_")
|> Enum.map(&String.capitalize/1)
|> Enum.join("")
end
defp grep_pattern(pattern) do
try do
{output, _} = System.cmd("grep", ["-r", pattern, "lib/"], stderr_to_stdout: true)
String.trim(output) != ""
rescue
_ -> false
end
end
end