Claude Skill

perf

Use when anything in a Phoenix app is slow, times out, or uses too much memory, even if the cause looks obvious. Load it before analyzing or fixing: it runs measured Ecto, LiveView and OTP checks and ranks fixes.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download oliver-kriska-claude-elixir-phoenix-plugins_elixir-phoenix_skills_perf-9767a82.zip · 4 KB
Part of oliver-kriska/claude-elixir-phoenix — 93 skills

Install

skills CLI npx skills add https://github.com/oliver-kriska/claude-elixir-phoenix/tree/main/plugins/elixir-phoenix/skills/perf
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install oliver-kriska-claude-elixir-phoenix@llmmart
Git git clone https://github.com/oliver-kriska/claude-elixir-phoenix.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole oliver-kriska/claude-elixir-phoenix collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Performance Analysis

Analyze code for performance issues across Ecto, LiveView, and OTP layers. Prioritize findings by impact and effort.

Usage

/phx:perf                           # Analyze full project
/phx:perf lib/my_app/accounts.ex    # Analyze specific module
/phx:perf --focus ecto              # Ecto queries only
/phx:perf --focus liveview          # LiveView memory only
/phx:perf --focus otp               # OTP bottlenecks only

Arguments

$ARGUMENTS = Optional module/context path and --focus flag.

Iron Laws

  1. MEASURE BEFORE OPTIMIZING — Never optimize without evidence of a problem
  2. DATABASE FIRST — 90% of Elixir performance issues are query-related
  3. ONE CHANGE AT A TIME — Isolate optimizations to measure impact
  4. NEVER benchmark in dev mode — Always use MIX_ENV=prod for performance measurements; dev mode includes code reloading, debug logging, and unoptimized compilation that invalidate results

Workflow

Step 1: Identify Scope

Check specific file if provided. Otherwise scan full project:

# Find hot paths: contexts, LiveViews, workers
find lib/ -name "*.ex" | head -50

Step 2: Run Analysis Tracks

Spawn analysis agents in parallel based on focus:

Ecto Track (default or --focus ecto):

Spawn phx:elixir-reviewer with prompt: "Analyze for N+1 queries, missing preloads, unindexed queries, and inefficient patterns. Check: Repo.all in loops, Enum.map with Repo calls, missing preload, queries without indexes on WHERE/JOIN columns."

LiveView Track (default or --focus liveview):

Spawn phx:elixir-reviewer with prompt: "Analyze LiveViews for memory issues: large assigns, missing streams for lists, assigns that grow unbounded, heavy handle_info processing, missing assign_async for slow ops."

OTP Track (only with --focus otp):

Spawn phx:otp-advisor with prompt: "Analyze for OTP bottlenecks: GenServer mailbox growth, synchronous calls in hot paths, missing Task.async for parallel work, ETS opportunities for read-heavy state."

Step 3: Prioritize Findings

Score each finding on a 2x2 matrix:

Low Effort High Effort
High Impact DO FIRST PLAN
Low Impact QUICK WIN SKIP

High impact = affects response time, memory per user, or query count. Low effort = single file change, no migration needed.

Step 4: Present Top 5

Present findings sorted by priority:

## Performance Analysis: {scope}

### 1. {Finding} — DO FIRST
**Impact**: {what improves}
**Location**: {file}:{line}
**Current**: {problematic pattern}
**Fix**: {optimized pattern}
**Estimated gain**: {e.g., "eliminates N+1, reduces queries from O(n) to O(1)"}

### 2. {Finding} — PLAN
...

Step 5: Offer Next Steps

Always end with actionable next steps — findings without follow-up get lost. Present options based on severity:

How would you like to proceed?

- `/phx:plan` — Create a plan from these findings (recommended for 3+ fixes)
- `/phx:quick` — Apply top priority fix directly (1-2 simple fixes)
- `/phx:investigate` — Deep-dive into a specific finding

Tidewave Integration

If Tidewave MCP is available:

  • Use mcp__tidewave__project_eval to run Repo.query!("EXPLAIN ANALYZE ...") on suspicious queries
  • Use mcp__tidewave__project_eval to check Process.info(pid, :message_queue_len) for GenServer bottlenecks
  • Use mcp__tidewave__execute_sql_query to check missing indexes

References

  • ${CLAUDE_SKILL_DIR}/references/benchmarking.md — Benchee patterns, profiling, flame graphs
Files (claude-elixir-phoenix)
  • references
    • benchmarking.md 5.2 KB
      # Benchmarking and Profiling
      
      ## Benchee Patterns
      
      ### Basic Benchmark
      
      ```elixir
      # In a script or IEx
      Benchee.run(%{
        "current" => fn -> MyModule.current_impl(data) end,
        "optimized" => fn -> MyModule.optimized_impl(data) end
      }, time: 10, memory_time: 2)
      ```
      
      ### Benchmark with Setup
      
      ```elixir
      Benchee.run(%{
        "preload" => fn {posts, _} ->
          Repo.preload(posts, :comments)
        end,
        "join" => fn {_, query} ->
          Repo.all(query)
        end
      }, before_each: fn _ ->
        posts = Repo.all(Post)
        query = from(p in Post, join: c in assoc(p, :comments), preload: [comments: c])
        {posts, query}
      end)
      ```
      
      ### Comparing Query Strategies
      
      ```elixir
      Benchee.run(%{
        "separate_queries" => fn ->
          posts = Repo.all(Post)
          Repo.preload(posts, :comments)
        end,
        "join_preload" => fn ->
          from(p in Post, join: c in assoc(p, :comments), preload: [comments: c])
          |> Repo.all()
        end,
        "subquery" => fn ->
          from(p in Post, preload: [comments: ^from(c in Comment, order_by: c.inserted_at)])
          |> Repo.all()
        end
      })
      ```
      
      ## Ecto Query Analysis
      
      ### EXPLAIN ANALYZE via Tidewave
      
      ```elixir
      # Check query plan for a specific query
      Repo.query!("EXPLAIN ANALYZE SELECT * FROM users WHERE email = $1", ["test@example.com"])
      ```
      
      ### Missing Index Detection
      
      ```sql
      -- Find sequential scans on large tables
      SELECT schemaname, relname, seq_scan, seq_tup_read,
             idx_scan, idx_tup_fetch
      FROM pg_stat_user_tables
      WHERE seq_scan > 100
      ORDER BY seq_tup_read DESC;
      ```
      
      ```sql
      -- Find tables without indexes on foreign keys
      SELECT c.conrelid::regclass AS table_name,
             a.attname AS column_name
      FROM pg_constraint c
      JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY(c.conkey)
      WHERE c.contype = 'f'
      AND NOT EXISTS (
        SELECT 1 FROM pg_index i
        WHERE i.indrelid = c.conrelid
        AND a.attnum = ANY(i.indkey)
      );
      ```
      
      ### N+1 Detection Patterns
      
      Common patterns that indicate N+1 queries:
      
      ```elixir
      # Pattern 1: Repo call inside Enum.map
      users
      |> Enum.map(fn user -> Repo.preload(user, :posts) end)
      # Fix: Repo.preload(users, :posts)
      
      # Pattern 2: Association access without preload
      for post <- posts do
        length(post.comments)  # Triggers lazy load per post
      end
      # Fix: posts = Repo.preload(posts, :comments)
      
      # Pattern 3: Repo.get inside comprehension
      for id <- user_ids do
        Repo.get!(User, id)
      end
      # Fix: Repo.all(from u in User, where: u.id in ^user_ids)
      ```
      
      ## LiveView Memory Profiling
      
      ### Assign Size Estimation
      
      ```elixir
      # Check socket assign sizes (in IEx with Tidewave)
      socket.assigns
      |> Enum.map(fn {key, val} -> {key, :erts_debug.size(val) * 8} end)
      |> Enum.sort_by(&elem(&1, 1), :desc)
      |> Enum.take(10)
      ```
      
      ### Process Memory Check
      
      ```elixir
      # Check LiveView process memory
      Process.info(pid, [:memory, :message_queue_len, :heap_size])
      ```
      
      ### Stream vs Assign Comparison
      
      | Metric | Regular Assign | Stream |
      |--------|---------------|--------|
      | Memory per item | Full struct | DOM patch |
      | Memory growth | O(n) items | O(1) patches |
      | Reconnect cost | Full list | Full list |
      | Append cost | Full list diff | Single item |
      
      Rule of thumb: Use streams when list > 100 items OR items
      are frequently updated.
      
      ## OTP Bottleneck Detection
      
      ### GenServer Mailbox
      
      ```elixir
      # Check if GenServer has mailbox buildup
      {:message_queue_len, len} = Process.info(pid, :message_queue_len)
      # len > 100 indicates bottleneck
      ```
      
      ### Observer Patterns
      
      ```elixir
      # Start observer for visual process tree
      :observer.start()
      
      # Or use runtime_tools for production
      :sys.get_state(pid)       # Current state (careful with large state)
      :sys.statistics(pid, :get) # Call/message statistics
      ```
      
      ### ETS vs GenServer Decision
      
      | Access Pattern | Use |
      |---------------|-----|
      | Read-heavy, write-rare | ETS (concurrent reads) |
      | Write-heavy | GenServer (serialized writes) |
      | Mixed, small state | GenServer |
      | Mixed, large state | ETS with GenServer for writes |
      
      ## Flame Graph Interpretation
      
      ### Generating Flame Graphs
      
      ```elixir
      # Using eflambe
      :eflambe.apply({MyModule, :my_function, [args]}, output_format: :brendan_gregg)
      
      # Using eflame (simpler)
      :eflame.apply(MyModule, :my_function, [args])
      ```
      
      ### Reading Flame Graphs
      
      - **Width** = time spent (wider = slower)
      - **Height** = call stack depth
      - Look for wide bars at the top (leaf functions consuming time)
      - Common culprits: `Enum.map`, `Jason.encode`, `Repo.query`
      
      ### Production-Safe Profiling
      
      ```elixir
      # Use :recon for production systems
      :recon.proc_count(:memory, 10)      # Top 10 by memory
      :recon.proc_count(:reductions, 10)  # Top 10 by CPU
      :recon.proc_count(:message_queue_len, 10)  # Top 10 by mailbox
      ```
      
      ## Performance Checklist
      
      ### Ecto
      
      - [ ] No `Repo.` calls inside `Enum.map/each/reduce`
      - [ ] Preloads use batch loading, not per-record
      - [ ] Frequently queried columns have indexes
      - [ ] Large result sets use `Repo.stream` or pagination
      - [ ] Aggregations done in SQL, not Elixir
      
      ### LiveView
      
      - [ ] Lists > 100 items use `stream/3`
      - [ ] Expensive operations use `assign_async/3`
      - [ ] No unbounded assign growth in `handle_info`
      - [ ] PubSub messages are small (IDs, not full structs)
      
      ### OTP
      
      - [ ] GenServers don't do heavy work in `handle_call`
      - [ ] Long operations use `Task.async` or `handle_continue`
      - [ ] No synchronous GenServer calls in request hot path
      - [ ] ETS used for read-heavy shared state
      
  • SKILL.md 3.9 KB
    ---
    name: perf
    description: "Use when anything in a Phoenix app is slow, times out, or uses too much memory, even if the cause looks obvious. Load it before analyzing or fixing: it runs measured Ecto, LiveView and OTP checks and ranks fixes."
    effort: high
    argument-hint: "[page|context|module] [--focus ecto|liveview|otp]"
    ---
    
    # Performance Analysis
    
    Analyze code for performance issues across Ecto, LiveView,
    and OTP layers. Prioritize findings by impact and effort.
    
    ## Usage
    
    ```
    /phx:perf                           # Analyze full project
    /phx:perf lib/my_app/accounts.ex    # Analyze specific module
    /phx:perf --focus ecto              # Ecto queries only
    /phx:perf --focus liveview          # LiveView memory only
    /phx:perf --focus otp               # OTP bottlenecks only
    ```
    
    ## Arguments
    
    `$ARGUMENTS` = Optional module/context path and `--focus` flag.
    
    ## Iron Laws
    
    1. **MEASURE BEFORE OPTIMIZING** — Never optimize without evidence of a problem
    2. **DATABASE FIRST** — 90% of Elixir performance issues are query-related
    3. **ONE CHANGE AT A TIME** — Isolate optimizations to measure impact
    4. **NEVER benchmark in dev mode** — Always use `MIX_ENV=prod` for performance measurements; dev mode includes code reloading, debug logging, and unoptimized compilation that invalidate results
    
    ## Workflow
    
    ### Step 1: Identify Scope
    
    Check specific file if provided. Otherwise scan full project:
    
    ```bash
    # Find hot paths: contexts, LiveViews, workers
    find lib/ -name "*.ex" | head -50
    ```
    
    ### Step 2: Run Analysis Tracks
    
    Spawn analysis agents in parallel based on focus:
    
    **Ecto Track** (default or `--focus ecto`):
    
    Spawn `phx:elixir-reviewer` with prompt:
    "Analyze for N+1 queries, missing preloads, unindexed queries,
    and inefficient patterns. Check: `Repo.all` in loops,
    `Enum.map` with Repo calls, missing `preload`, queries without
    indexes on WHERE/JOIN columns."
    
    **LiveView Track** (default or `--focus liveview`):
    
    Spawn `phx:elixir-reviewer` with prompt:
    "Analyze LiveViews for memory issues: large assigns, missing
    streams for lists, assigns that grow unbounded, heavy
    `handle_info` processing, missing `assign_async` for slow ops."
    
    **OTP Track** (only with `--focus otp`):
    
    Spawn `phx:otp-advisor` with prompt:
    "Analyze for OTP bottlenecks: GenServer mailbox growth,
    synchronous calls in hot paths, missing Task.async for
    parallel work, ETS opportunities for read-heavy state."
    
    ### Step 3: Prioritize Findings
    
    Score each finding on a 2x2 matrix:
    
    | | Low Effort | High Effort |
    |---|---|---|
    | **High Impact** | DO FIRST | PLAN |
    | **Low Impact** | QUICK WIN | SKIP |
    
    High impact = affects response time, memory per user, or query count.
    Low effort = single file change, no migration needed.
    
    ### Step 4: Present Top 5
    
    Present findings sorted by priority:
    
    ```markdown
    ## Performance Analysis: {scope}
    
    ### 1. {Finding} — DO FIRST
    **Impact**: {what improves}
    **Location**: {file}:{line}
    **Current**: {problematic pattern}
    **Fix**: {optimized pattern}
    **Estimated gain**: {e.g., "eliminates N+1, reduces queries from O(n) to O(1)"}
    
    ### 2. {Finding} — PLAN
    ...
    ```
    
    ### Step 5: Offer Next Steps
    
    Always end with actionable next steps — findings without follow-up
    get lost. Present options based on severity:
    
    ```
    How would you like to proceed?
    
    - `/phx:plan` — Create a plan from these findings (recommended for 3+ fixes)
    - `/phx:quick` — Apply top priority fix directly (1-2 simple fixes)
    - `/phx:investigate` — Deep-dive into a specific finding
    ```
    
    ## Tidewave Integration
    
    If Tidewave MCP is available:
    
    - Use `mcp__tidewave__project_eval` to run `Repo.query!("EXPLAIN ANALYZE ...")` on suspicious queries
    - Use `mcp__tidewave__project_eval` to check `Process.info(pid, :message_queue_len)` for GenServer bottlenecks
    - Use `mcp__tidewave__execute_sql_query` to check missing indexes
    
    ## References
    
    - `${CLAUDE_SKILL_DIR}/references/benchmarking.md` — Benchee patterns, profiling, flame graphs
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related