Lessons from Optimizing
Phoenix Integration Tests

From 10-Minute Black Box to 3-Minute Transparent CI

Rodolfo Carvalho
Elixir Vienna Meetup
September 24, 2026
Lessons from Optimizing Phoenix Integration Tests

About Me & Praia Labs

Background Building with LiveView

  • Built and scaled SaaS product on Phoenix LiveView over the past few years.
  • Now consulting through Praia Labs, helping teams design, build, and scale applications with Phoenix & LiveView.
  • Navigating AI-assisted development, mostly staying in the driver's seat.

Philosophy Caring for Dependencies

  • Long-time contributor to Phoenix, LiveView, Hex, and the Elixir ecosystem.
  • Why work upstream?
    • A strong belief in giving back and actively caring for our dependencies.
    • Investing in upstream speed and stability benefits every developer in the room.

Waiting for Long Builds

"The #1 Programmer Excuse for Legitimately Slacking Off: My code's compiling." (XKCD #303)

Lessons from Optimizing Phoenix Integration Tests

Context: Testing Phoenix Itself (Upstream)

Scope Framework, Not an App

We are not talking about testing a typical app built with Phoenix.

We are testing Phoenix itself (phoenixframework/phoenix):

  • Testing code generation output end-to-end.
  • Matrix across supported Elixir & OTP versions.
  • Compatibility with 4 database engines: PostgreSQL, MySQL, MSSQL, SQLite3.

Universal Why This Matters to You

The lessons and wins apply to any project:

  • ExUnit concurrency model: How tests actually get scheduled across CPU cores.
  • Observability: Custom formatters & CI step summaries.
  • Universal speedups: In-memory tmpfs RAM disks, caching tricks, and Docker ergonomics.
Lessons from Optimizing Phoenix Integration Tests

Phoenix Test Taxonomy: What Tests What?

The Phoenix repository contains 4 distinct test suites:

  • Unit & Core /test:
    Unit & functional tests for framework internals (Router, Endpoint, Channels, PubSub).
  • Installer /installer/test:
    Validates mix phx.new scaffolding logic (does it generate expected files & trees?).
  • Generators /test/mix/tasks/:
    Validates phx.gen.html, phx.gen.live, phx.gen.auth code generation against mocked apps.
  • Integration /integration_test:
    The real deal. Generates a complete app on disk, runs generators, compiles with --warnings-as-errors, verifies mix format, connects to real databases, migrates, and runs mix test inside the generated app!
Lessons from Optimizing Phoenix Integration Tests

The "Integration Test Tax" & The Black Box

  • Every Phoenix PR carried an opaque ~10-minute CI tax.
  • Worse than dots (....):
    • CI runners buffer standard output line-by-line or by chunk.
    • Because ExUnit outputs progress dots without newlines, dots didn't stream interactively!
    • You literally stared at an empty spinning wheel for 8–10 minutes with zero feedback.
  • The Developer Anxiety:
    • Did MSSQL hang on startup?
    • Is mix deps.get stalled on a network timeout?
    • Did compilation fail?
    • Nobody could tell until the whole job finished or timed out.
Lessons from Optimizing Phoenix Integration Tests

You Can't Optimize What You Can't Measure

The Intuitive First Attempt: mix test --slowest

  • ExUnit provides --slowest N and --slowest-modules.
  • The Profiling Trap:
    • In ExUnit, passing --slowest automatically forces --trace!
    • --trace sets --max-cases 1 (forcing all async tests to run serially) and sets timeout: :infinity!
    • Profiling destroyed the suite's concurrency and blew up wall-clock time!
  • Job-Level Opacity:
    • Monolithic test.sh bundled system packages, DB boot, dependency fetch, compilation, and test execution into a single timed blob.
Lessons from Optimizing Phoenix Integration Tests

Stage 1: Gaining Visibility (PR #6825 & PR #6817)

CI Step Decomposition (PR #6825)

  • Replaced monolithic test.sh with discrete GitHub Actions workflow steps.
  • Surfaced exact phase timings in the Actions UI:
    • Package install: ~15s
    • DB healthcheck wait: ~20s
    • Deps & NIF compile: ~45s
    • Test execution: ~8m+

SummaryFormatter (PR #6817)

  • Custom lightweight ExUnit formatter.
  • Runs at full async concurrency (max_cases: 8).
  • Preserves active timeout protection.
  • Appends a structured Markdown duration table directly to $GITHUB_STEP_SUMMARY!
Lessons from Optimizing Phoenix Integration Tests

Baseline Timeline: The Monolith in Action

Timeline rendered by SummaryFormatter before splitting:

All 7 modules launch at 00:00. Fast modules finish early; 2 stragglers drag past 8 minutes.

Lessons from Optimizing Phoenix Integration Tests

The Straggler Effect: vCPUs Idling

  • Standard GitHub Actions Linux runners have 4 vCPUs (16 GB RAM).
  • What happened during execution:
    • : ExUnit starts all 7 modules (8 slots available).
    • : Fast modules finish (AppWithNoOptionsTest).
    • : Most modules finish.
    • to :
      • 1~2 cores still executing tests.
      • Other vCPUs sit completely idle doing nothing!
  • The entire test suite wall-clock time was bounded by monolithic stragglers.
Lessons from Optimizing Phoenix Integration Tests

Revelation #1: ExUnit's Scheduling Model

  • How does ExUnit run asynchronous tests?
    • ExUnit parallelizes across modules (async: true).
    • BUT tests within a single module run serially on one process!
  • Phoenix had only 7 monolithic test files:
    1. AppWithDefaultsTest (12 tests)
    2. UmbrellaAppWithDefaultsTest (10 tests)
    3. AppWithScopesTest (8 tests)
    4. AppWithMySqlAdapterTest (6 tests)
    5. AppWithMSSQLAdapterTest (6 tests)
    6. AppWithSQLite3AdapterTest (6 tests)
    7. AppWithNoOptionsTest (5 tests)

Famous Last Words...

August 31: "I'm not signing up for that right now :)"
Spoiler: He went on to sign up for exactly that.

"I'll Just Automate It!"

"Theory: Work on original task permanently reduced. Reality: Rethinking, debugging, ongoing development..." (XKCD #1319)

Lessons from Optimizing Phoenix Integration Tests

Stage 2: Breaking the Monolith (PR #6826)

  • Split 7 monolithic files into 33 focused async modules:
    • app_with_postgres_adapter_auth_html_test.exs
    • app_with_postgres_adapter_auth_live_test.exs
    • app_with_postgres_adapter_html_test.exs
    • app_with_postgres_adapter_json_test.exs
    • app_with_postgres_adapter_live_test.exs
    • ...and symmetrical counterparts for MySQL, MSSQL, SQLite, & Umbrella!
  • Guarantees:
    • Preserved all 53 original tests.
    • Every module designed to execute in under 3 minutes.
    • ExUnit now dynamically saturates all runner cores from start to finish!
Lessons from Optimizing Phoenix Integration Tests

Trust but Verify: The AST Comparison Script

How do you refactor 53 critical tests across 33 files without missing assertions?

defmodule CompareSuites do
  def run do
    main_tests = Enum.flat_map(git_files("upstream/main"), &parse_git_file/1)
    head_tests = Enum.flat_map(git_files("HEAD"), &parse_git_file/1)
    
    # 1. Parse AST with Code.string_to_quoted!/1
    # 2. Extract describe, test names, and tags
    # 3. Normalize app names: "pg_auth_live" -> "__APP__"
    # 4. Assert byte-for-byte / AST equivalence of every test body!
    match_and_diff_tests(main_tests, head_tests)
  end
end

✅ Verified all 53 test bodies were 100% equivalent before opening the PR.

Lessons from Optimizing Phoenix Integration Tests

Greedy Interval Scheduling for Gantt Charts

  • Problem: Rendering 33 individual rows in a Gantt chart produces an unreadable 33-line wall of text.
  • Solution in SummaryFormatter:
    • Implemented greedy interval scheduling directly in Elixir!
    • Assigns non-overlapping test modules to compact virtual worker lanes (Lane 1 to Lane 8).
    • Automatically tags the critical path module with :crit.
defp assign_to_lane([], item, acc), do: Enum.reverse([[item] | acc])
defp assign_to_lane([[{_mod, %{finish: f}} | _] = lane | rest], {_m, %{start: s}} = item, acc) do
  if f <= s, do: Enum.reverse(acc) ++ [[item | lane] | rest],
  else: assign_to_lane(rest, item, [lane | acc])
end
Lessons from Optimizing Phoenix Integration Tests

Compact Gantt Timeline (Worker Lanes)

All 8 ExUnit worker slots stay saturated across 4 vCPUs. No worker sits idle waiting for a monolithic file.

Lessons from Optimizing Phoenix Integration Tests

Infrastructure: Host Runners & tmpfs (PR #6831)

  • Migrated from Alpine Container to Native Ubuntu Runner:
    • Ran directly on ubuntu-24.04 host using erlef/setup-beam.
    • Pre-installed system build tools (no more apk add latency).
    • Removed background socat TCP proxy bridges (ports bind directly to localhost).
  • Dependency & Build Caching:
    • Enabled actions/cache for deps/ and _build/ keyed on mix.lock.
    • Saves ~50 seconds of dependency re-compilation on warm runs!
  • Mounted installer/tmp on tmpfs (RAM Disk):
    • Integration tests generate apps, copying dependencies & compiling code repeatedly.
    • Running file I/O entirely in RAM eliminated disk bottlenecks!
Lessons from Optimizing Phoenix Integration Tests

Sharding: --test-partition vs DB Sharding

Steffen Deusch and I explored two horizontal scaling strategies:

Generic Partitions (PR #6834)

mix test --test-partition 4

  • Great for self-contained unit tests!
  • The flaw for integration tests: ExUnit hashes test files across partitions.
  • Every partition runner gets a random mix of Postgres, MySQL, & MSSQL tests.
  • Every runner must boot all 3 DBs!

Database Sharding (PR #6836)

[postgresql], [mysql], [mssql], [none]

  • Shard strictly by backing database service.
  • Containers boot on demand:
    • [sqlite3 + no-db] boots 0 containers!
    • Postgres runner boots only Postgres.
  • Mounts DB storage in tmpfs.
  • Saturates 8 runners across 32 vCPUs!
Lessons from Optimizing Phoenix Integration Tests

Cross-Shard Summary Aggregation

  • The Problem: 8 parallel CI jobs = 8 separate GitHub Step Summaries.
    • Fragmented, cluttered, and hard to spot the overall critical path.
  • The Solution (aggregate_summary.exs):
    1. Each shard writes a lightweight summary.json (a few KB).
    2. Uploaded as temporary workflow artifacts.
    3. A final aggregator job downloads the JSON files and compiles a unified dashboard:
Elixir/OTP Job Status Tests Wall Time Slowest Test
1.20 / 29 Combined Passed 53 3m 19s UmbrellaPostgres: auth live (42s)
PostgreSQL Passed 24 3m 19s UmbrellaPostgres: auth live (42s)
SQLite3 + no-DB Passed 14 2m 38s UmbrellaDefaults: defaults (36s)
MySQL Passed 8 2m 04s AppWithMySQL: auth live (38s)
MSSQL Passed 7 1m 32s AppWithMSSQL: auth html (32s)
Lessons from Optimizing Phoenix Integration Tests

The Payoff: 10 Minutes 3m 35s!

Real CI wall-clock metrics from GitHub Actions Run 33897992674:

10m+
ORIGINAL WALL TIME
3m 35s
OPTIMIZED WALL TIME
~65%
TOTAL TIME SAVED
  • Breakdown:
    npm test: 17s
    installer test: 36s
    mix test: 56s
    integration test [mssql]: 1m 32s
    integration test [mysql]: 2m 04s
    integration test [sqlite3 + no-db]: 2m 38s
    integration test [postgresql]: 3m 19s (Critical Path)
    integration test results (aggregator): 12s
Lessons from Optimizing Phoenix Integration Tests

AI Pair Programming: Blessing & Curse

Using AI coding assistants (Gemini / Antigravity) throughout this journey:

The Blessing ⚡

  • Rapid prototyping: Testing out interval scheduling algorithms in minutes.
  • AST parser generation: Generating the CompareSuites verification script.
  • Git archaeology: Tracing historical directory leaks back to 2014 commits.

The Curse ⚠️

  • Subtle bugs: Generated app names exceeded default 98-char formatter limits!
  • Phantom edits: Expanding/contracting words in test descriptions (it'd $\leftrightarrow$ it would).
  • Domain socket pitfalls: Container healthchecks probing Unix sockets instead of TCP.

High speed requires even higher vigilance. "That's the times we live in."

Software Testing Day

"Celebrated every -1 years on January 0th at 25:71 PM." (XKCD #2928)

Lessons from Optimizing Phoenix Integration Tests

Key Takeaways

  1. Measure Before Optimizing:
    Beware of built-in flags that alter runtime semantics (like mix test --slowest forcing --trace).
  2. ExUnit Concurrency Model:
    async: true parallelizes modules, not tests. Break up monolithic files to keep all CPU cores hot.
  3. Sharding with Services:
    Generic test partitioning is great for unit tests; service-aware sharding is king when containers are involved.
  4. I/O Matters:
    Mounting ephemeral test databases and temporary output on tmpfs is basically free speed.
  5. Observability as Code:
    Custom ExUnit formatters and $GITHUB_STEP_SUMMARY can turn raw CI dots into transparent engineering dashboards.

Thank You! Questions?

Rodolfo Carvalho
GitHub: @rhcarvalho
QR Code for Slides
praialabs.github.io/optimizing-phoenix-ci
💜 Special thanks to Steffen Deusch for maintaining Phoenix, exploring partition schemes, and reviewing PRs with patience and great insights!
Appendix

Evolution from 2020 Scaffolding to 2024 Reality

The Origin (2020 - 2021)

  • Late 2020: Aaron Renner introduces /integration_test (PRs #3970, #4027).
  • Jan 2021: Blog post celebrates Earthly for reproducible local & CI builds.
  • Containerized build matrix with Docker Compose backing services.

The Drift (2021 - 2024)

  • May 2024: Earthly dropped by Steffen Deusch (PR #5817) due to local developer friction.
  • Replaced with simple docker.sh and Docker Compose.
  • Leftover CI debt: Tests still executed via a monolithic test.sh inside an Alpine Docker container.
Appendix

Local Workflow Ergonomics

CI improvements should never make local development painful!

  • Apple Silicon (arm64) Auto-Detection in docker.sh:
    • Official Microsoft SQL Server image only supports amd64.
    • Detects architecture via uname -m; automatically skips MSSQL on ARM64 macs instead of crashing under emulation.
  • Warm Database Containers:
    • docker.sh keeps database containers alive between runs.
    • Re-running tests has zero container boot latency!
    • Run ./docker.sh down when you're done for the day.
  • Non-Root & Port Isolation:
    • Container runs as non-root user (no root-owned file pollution).
    • Named volumes for native NIFs (bcrypt, argon2).
    • Database ports bind to loopback (127.0.0.1), preventing host port collisions.
Appendix

Micro-Wins (AI-Induced Tangents)

Eliminating Blind Sleeps (PR #6830)

  • Migration versions use 1-second timestamps (YYYYMMDDHHMMSS).
  • Chained generators (phx.gen.auth then phx.gen.live) called Process.sleep(1500) 8 times to prevent collisions.
  • Added adjust_migration_timestamps/1:
    • Backdates migrations on disk deterministically into the past.
    • Subsequent generators create fresh timestamps instantly with zero sleep latency!

10-Year Directory Leak (PR #6833)

  • Consolidated generated app output into installer/tmp/.
  • Git archaeology: Found that with_installer_tmp/3 had been leaking empty random directories since 2014!
  • Fixed the directory leak, leaving clean disk space for in-memory tmpfs mounts.
Appendix

Upstream Pull Requests

  • PR #6817: Add SummaryFormatter for integration test visibility in CI
  • PR #6825: Decompose CI workflow steps & improve container reliability
  • PR #6826: Split integration test suites into 33 async modules & lane scheduling
  • PR #6830: Eliminate Process.sleep via backdated migration timestamps
  • PR #6831: Run integration tests on host runner in CI with actions/cache & tmpfs
  • PR #6833: Consolidate temporary app generation to installer/tmp and fix directory leak
  • PR #6836: Shard integration tests by database with on-demand container setup in CI
  • PR #6873: Improve local integration test workflow

Speaker notes: - Welcome everyone! Thank you for having me at Elixir Vienna. - Tonight I want to share a hands-on, practical journey deep into the Phoenix Framework test infrastructure. - If you've contributed to Phoenix or run large test suites in your own production apps, you know the pain of slow, opaque CI. - We're going to see how we took Phoenix integration tests from 10 minutes down to ~3 minutes, and the hard-won lessons along the way.

Speaker notes: - A quick 30 seconds on where I'm coming from: - For the past few years, I built a SaaS product entirely on Phoenix LiveView. - Recently, I launched Praia Labs, where I consult and help other engineering teams build, optimize, and scale production systems with Phoenix and LiveView. - Throughout this journey, I've contributed upstream to Phoenix, LiveView, Hex, and other Elixir core tooling. - I believe deeply in giving back and caring for our dependencies—when upstream tools are fast, transparent, and pleasant to work with, the entire ecosystem wins.

Speaker notes: - A mandatory slide for any CI talk! - 10 minutes is that awkward duration: too long to stare at the screen, too short to start working on a completely different deep feature. - You lose context, check social media, grab coffee, and lose flow.

Speaker notes: - Important clarification right at the start! - When people see "Phoenix testing", they usually assume it's about testing an app built with Phoenix (like testing controllers or LiveViews). - This is about testing the framework itself upstream in the core Phoenix repository. - Every release of Phoenix must guarantee that `mix phx.new` and all generators produce 100% valid, warning-free, perfectly formatted code that passes all tests across multiple Elixir, OTP, and database combinations. - BUT: the techniques we'll cover—ExUnit scheduling, CI formatting, tmpfs, container caching—are universal and apply to every team in this room.

Speaker notes: - Important to establish this boundary up front. - People often think installer tests are integration tests. - But installer/test just checks if templates render without running them. - /integration_test actually generates the whole application, boots live databases (Postgres, MySQL, MSSQL, SQLite), drops/creates databases, runs migrations, and invokes `mix test` inside the generated project.

Speaker notes: - As a long-time contributor, this was deeply personal. Every small PR meant waiting 10 minutes. - The buffering detail is subtle but brutal: ExUnit prints single dots as tests pass. But GitHub Actions buffers output. So you don't even get dots! - You just see a spinning circle with no lines emitted for nearly 10 minutes.

Speaker notes: - The classic observer effect in performance debugging! - You try to inspect what's slow using standard tooling (`--slowest`), but doing so changes the execution semantics from concurrent to serial. - So the very act of profiling made the test run vastly slower.

Speaker notes: - Now we can measure without destroying concurrency! - PR 6825 broke down the shell script into distinct CI steps, so we could see how long each phase took. - PR 6817 created SummaryFormatter, turning GitHub Step Summary into a rich observability dashboard. - Now every PR showed exactly which test modules took the most time.

Speaker notes: - Look at this Mermaid chart—this was rendered directly into GITHUB_STEP_SUMMARY! - You can immediately see the problem: 7 bars, all starting at 0, but UmbrellaAppWithDefaultsTest drags out to 8 minutes 14 seconds. - After 6 minutes, almost everything else is done.

Speaker notes: - With 4 vCPUs, BEAM has 4 schedulers, so ExUnit provides 8 parallel worker slots by default. - But Phoenix only had 7 monolithic test files! Even at T=0, we couldn't saturate the runner. - And once the quick files finished, one worker grinded away past 8 minutes while everything else sat completely idle. - That idle tail is pure wasted wall-clock time.

Speaker notes: - This is the single most important technical takeaway for anyone writing Elixir tests. - When you write `use ExUnit.Case, async: true`, you might think all `test "..."` blocks run concurrently. - No! ExUnit spawns one worker process per module. All tests inside that module run one after another in serial order. - Parallelism only happens across different test files/modules.

Speaker notes: - This is a real quote from August 31st during early architecture discussions with Steffen. - With 50+ tests, could we run more than 8 concurrently? - My immediate, rational response: "I'm not signing up for that right now :) (it takes a lot of time iterating on this, AI or not)". - It felt like too big of an undertaking with too many moving pieces. - But as engineers... the temptation to fix the inefficiency never really leaves your mind. - Which brings us directly to...

Speaker notes: - Theory: "I'll just split those 7 files into smaller files, easy afternoon project!" - Reality: Welcome to AST parsing, test matching, Elixir formatter limits, and database concurrency collisions!

Speaker notes: - This was PR #6826. - Instead of grouping by database adapter and running 10 features serially, we split each feature permutation into its own module. - Now, when a core finishes a test file, ExUnit immediately feeds it the next file from the queue of 33 modules. - Zero worker starvation.

Speaker notes: - With 53 tests in a foundational framework, manual review isn't enough. - We built an Elixir script using `Code.string_to_quoted!/1` to parse the AST of upstream/main vs HEAD. - It normalized the generated app names (replacing "pg_auth_live" with "__APP__") and verified that the test bodies were byte-for-byte identical. - Fun anecdote: The AI assistant actually made subtle edits like expanding contractions ("it'd" -> "it would") in test names! So even with an AST matcher, there was still a human element of scrutiny and hope!

Speaker notes: - If you output 33 rows in Mermaid, the chart is gigantic and unreadable on GitHub. - So we used a classic greedy interval scheduling algorithm: when a test finishes, place the next test that started after it into the same lane. - This compresses the suite into 8 tidy lanes, matching ExUnit's 8 concurrent worker slots (4 schedulers × 2)!

Speaker notes: - Look at the difference compared to the first chart! - All 8 lanes are constantly working. When one module finishes, the next one immediately fills the slot. - Total wall time dropped significantly because we eliminated the idle worker tail.

Speaker notes: - Big infrastructure shift in PR #6831. - Why run inside a nested Docker container when GitHub provides a powerful host runner? - Running on the host unlocked actions/cache (saving ~50s on dependencies) and let us mount a 4GB tmpfs RAM disk for all temporary generated apps.

Speaker notes: - This is a critical comparison. - `mix test --test-partition` is a wonderful built-in feature in Elixir. Everyone should use it for standard unit tests! - But when your tests have heavy external service dependencies, generic hashing forces every runner to start every database. - Explicit database sharding meant sqlite3 starts instantly with zero Docker overhead, and each DB shard only runs the container it actually needs.

Speaker notes: - With 8 shards, the GitHub Step Summary UI got messy. - So we built an aggregation script: each job drops a JSON artifact, and a 10-second final step joins them all into a single, clean overview table. - You can instantly see that PostgreSQL is the critical path at 3m19s.

Speaker notes: - Here are the actual numbers from GitHub Actions! - Total run started at 16:57:42 and finished at 17:01:17. - Total wall-clock time: 3 minutes 35 seconds! - PostgreSQL is the longest shard at 3m 19s, meaning that is our theoretical limit without splitting PostgreSQL further.

Speaker notes: - Let's be honest about AI tools. Everyone in the room is either using them or curious about them. - AI was incredible for generating the AST comparison script and interval scheduling logic in minutes. - But it introduced subtle bugs: for example, naming an app "app_with_postgres_adapter_auth_live" made the generated router file exceed Elixir's default 98-character line length, causing `mix format` to fail! - You must remain vigilant. Trust, but verify with automated tests.

Speaker notes: - Comic 2928 from May 2024. - Alt text: "The company tried to document how often employees were celebrating Software Testing Day, but their recordkeeping system kept mysteriously crashing." - Edge cases are what we live for in testing.

Speaker notes: - 5 practical rules you can take back to your own projects tomorrow morning. - Especially #2 and #4: check your longest test files, split them up, and use tmpfs in CI.

Speaker notes: - Thank you everyone for your time! - A huge, heartfelt thank you to Steffen Deusch (@SteffenDE). He has been doing fantastic work maintaining Phoenix and spent hours exploring partition ideas with me and reviewing these PRs. - Also thanks to Chris McCord, José Valim, and the whole Phoenix Core team for building and stewarding such an incredible framework. - Happy to take any questions!

Speaker notes: - Aaron Renner's 2021 blog post "Improving Testing & Continuous Integration in Phoenix" laid the groundwork. - Earthly had a great promise: run CI builds locally with identical containers. - But by 2024, it became a point of friction for contributors when things broke locally. - When Earthly was removed in PR #5817, what was left in CI was a monolithic `test.sh` script running inside a nested Alpine container.

Speaker notes: - Don't optimize CI at the expense of local developers! - On Apple Silicon, MSSQL crashes or crawls under Rosetta emulation. We detect ARM64 and cleanly skip MSSQL locally. - Backing containers stay warm between test runs, so iterating locally is instantaneous.

Speaker notes: - Two nice hygiene PRs that cleaned up technical debt. - The 1500ms sleep tax: 8 instances of sleeping 1.5 seconds. By backdating files on disk, we eliminated all sleeps entirely. - And the directory leak: dating back to when phoenix.new was extracted in 2014/2015. Clean teardown enabled the next big optimization: tmpfs.

Speaker notes: - Annex slide for distribution and reference. - All PRs are open or merged upstream at github.com/phoenixframework/phoenix.