Current section

Files

Jump to
phoenix_test_playwright lib phoenix_test playwright browser_pool.ex
Raw

lib/phoenix_test/playwright/browser_pool.ex

defmodule PhoenixTest.Playwright.BrowserPool do
@moduledoc """
Experimental browser pooling. Reuses browsers across test suites.
This limits memory usage and is useful when running feature tests together with regular tests
(high ExUnit `max_cases` concurrency such as the default: 2x number of CPU cores).
Pools are defined up front.
Browsers are launched lazily.
Usage:
```ex
# test/test_helper.exs
{:ok, _} =
Supervisor.start_link(
[
{PhoenixTest.Playwright.BrowserPool, name: :normal_chromium, size: System.schedulers_online(), browser: :chromium},
{PhoenixTest.Playwright.BrowserPool, name: :slow_chromium, size: 4, browser: :chromium, slow_mo: 100},
],
strategy: :one_for_one
)
# configure pool per test module
# test/my_test.exs
defmodule PhoenixTest.PlaywrightBrowserPoolTest do
use PhoenixTest.Playwright.Case,
async: true,
browser_pool: :normal_chromium
end
# or configure globally
# test/test.exs
config :phoenix_test,
playwright: [
browser_pool: :normal_chromium,
browser_pool_checkout_timeout: to_timeout(minute: 10)
]
```
"""
use GenServer
alias __MODULE__, as: State
alias PhoenixTest.Playwright
defstruct [
:size,
:config,
available: [],
in_use: %{},
waiting: []
]
@type pool :: GenServer.server()
@type browser_id :: binary()
## Public
@spec checkout(pool()) :: browser_id()
def checkout(pool) do
timeout = Playwright.Config.global(:browser_pool_checkout_timeout)
GenServer.call(pool, :checkout, timeout)
end
## Internal
@doc false
def start_link(opts) do
{name, opts} = Keyword.pop!(opts, :name)
{size, opts} = Keyword.pop!(opts, :size)
GenServer.start_link(__MODULE__, %State{size: size, config: opts}, name: name)
end
@impl GenServer
def init(state) do
# Trap exits so we can clean up browsers on shutdown
Process.flag(:trap_exit, true)
{:ok, state}
end
@impl GenServer
def handle_call(:checkout, from, state) do
cond do
length(state.available) > 0 ->
browser_id = hd(state.available)
state = do_checkout(state, from, browser_id)
{:reply, browser_id, state}
map_size(state.in_use) < state.size ->
browser_id = launch(state.config)
state = do_checkout(state, from, browser_id)
{:reply, browser_id, state}
true ->
state = Map.update!(state, :waiting, &(&1 ++ [from]))
{:noreply, state}
end
end
@impl GenServer
def handle_info({:DOWN, ref, :process, pid, _reason}, state) do
case Enum.find_value(state.in_use, fn {browser_id, tracked} -> tracked == {pid, ref} and browser_id end) do
nil -> {:noreply, state}
browser_id -> {:noreply, do_checkin(state, browser_id)}
end
end
defp launch(config) do
config = config |> Playwright.Config.validate!() |> Keyword.take(Playwright.Config.setup_all_keys())
{type, config} = Keyword.pop!(config, :browser)
Playwright.Connection.launch_browser(type, config)
end
defp do_checkout(state, from, browser_id) do
{from_pid, _tag} = from
state
|> Map.update!(:available, &(&1 -- [browser_id]))
|> Map.update!(:in_use, &Map.put(&1, browser_id, {from_pid, Process.monitor(from_pid)}))
end
defp do_checkin(state, browser_id) do
{{_from_pid, ref}, in_use} = Map.pop(state.in_use, browser_id)
Process.demonitor(ref, [:flush])
state = %{state | in_use: in_use, available: [browser_id | state.available]}
case state.waiting do
[from | rest] ->
GenServer.reply(from, browser_id)
%{do_checkout(state, from, browser_id) | waiting: rest}
_ ->
state
end
end
end