Skip to content

Optimize LiveView async PID collection#4235

Closed
preciz wants to merge 1 commit into
phoenixframework:mainfrom
preciz:optimize-live-asyncs-aggregation
Closed

Optimize LiveView async PID collection#4235
preciz wants to merge 1 commit into
phoenixframework:mainfrom
preciz:optimize-live-asyncs-aggregation

Conversation

@preciz
Copy link
Copy Markdown
Contributor

@preciz preciz commented May 16, 2026

Build component async lookup in a single pass instead of creating per-component maps and repeatedly merging them.

Bench shows the new async aggregation is ~1.23x-1.87x faster across the tested inputs, with memory usage reduced by ~19%-49% when async refs are present and effectively unchanged for the no-async case.

Bench:

Mix.install([{:benchee, "~> 1.5"}])

defmodule Before do
  def all_asyncs(state) do
    state.socket.private
    |> socket_asyncs(nil)
    |> Map.merge(component_asyncs(state))
  end

  defp component_asyncs(%{components: {components, _ids, _}}) do
    Enum.reduce(components, %{}, fn {cid, {_mod, _id, _assigns, private, _prints}}, acc ->
      Map.merge(acc, socket_asyncs(private, cid))
    end)
  end

  defp socket_asyncs(%{live_async: ref_pids}, cid) do
    Enum.into(ref_pids, %{}, fn {key, {ref, pid, kind}} -> {pid, {key, ref, cid, kind}} end)
  end

  defp socket_asyncs(%{}, _cid), do: %{}
end

defmodule After do
  def all_asyncs(state) do
    state.socket.private
    |> socket_asyncs(nil)
    |> Map.merge(component_asyncs(state))
  end

  defp component_asyncs(%{components: {components, _ids, _}}) do
    for {cid, {_mod, _id, _assigns, %{live_async: ref_pids}, _prints}} <- components,
        {key, {ref, pid, kind}} <- ref_pids,
        into: %{} do
      {pid, {key, ref, cid, kind}}
    end
  end

  defp socket_asyncs(%{live_async: ref_pids}, cid) do
    Map.new(ref_pids, fn {key, {ref, pid, kind}} -> {pid, {key, ref, cid, kind}} end)
  end

  defp socket_asyncs(%{}, _cid), do: %{}
end

defmodule Inputs do
  def state(component_count, async_every, refs_per_component, root_refs \\ 0) do
    components =
      Map.new(1..component_count, fn cid ->
        private =
          if async_every != :none and rem(cid, async_every) == 0 do
            %{live_async: refs({:component, cid}, refs_per_component)}
          else
            %{}
          end

        {cid, {:mod, cid, %{}, private, nil}}
      end)

    socket_private =
      if root_refs == 0, do: %{}, else: %{live_async: refs(:root, root_refs)}

    %{components: {components, nil, nil}, socket: %{private: socket_private}}
  end

  defp refs(prefix, count) do
    Map.new(1..count, fn i ->
      {{prefix, i}, {make_ref(), spawn(fn -> Process.sleep(:infinity) end), :task}}
    end)
  end
end

Benchee.run(
  %{
    "before" => &Before.all_asyncs/1,
    "after" => &After.all_asyncs/1
  },
  inputs: %{
    "100 comps, no asyncs" => Inputs.state(100, :none, 0),
    "100 comps, 10 asyncs x1" => Inputs.state(100, 10, 1),
    "100 comps, 10 asyncs x1, root x10" => Inputs.state(100, 10, 1, 10),
    "100 comps, all asyncs x1" => Inputs.state(100, 1, 1),
    "1000 comps, 100 asyncs x1" => Inputs.state(1000, 10, 1)
  },
  warmup: 1,
  time: 3,
  memory_time: 1,
  pre_check: true,
  print: [fast_warning: false]
)

Build component async lookup in a single pass instead of creating per-component maps and repeatedly merging them. Use Map.new/2 for socket async refs to avoid comprehension overhead on non-empty maps.
@preciz preciz changed the title Optimize asyncs aggregation using for comprehensions Optimize asyncs aggregation using for comprehension May 16, 2026
@preciz preciz changed the title Optimize asyncs aggregation using for comprehension Optimize LiveView async PID collection May 16, 2026
@SteffenDE
Copy link
Copy Markdown
Collaborator

Thank you for looking into this. Again, similar to #4218, as this is for tests only and also spreads the logic into multiple places, I'm not convinced we should go with this.

@SteffenDE SteffenDE closed this May 18, 2026
@preciz
Copy link
Copy Markdown
Contributor Author

preciz commented May 18, 2026

Sorry, totally forgot this was already opened.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants