Skip to content

Remove dynamic apply/3 call when it's not needed#1321

Closed
saleyn wants to merge 1 commit into
elixir-plug:mainfrom
saleyn:json
Closed

Remove dynamic apply/3 call when it's not needed#1321
saleyn wants to merge 1 commit into
elixir-plug:mainfrom
saleyn:json

Conversation

@saleyn

@saleyn saleyn commented Jul 23, 2026

Copy link
Copy Markdown

Implements a high‑performance Plug JSON parser variant that:

  • precomputes the decoder and stores it in persistent_term
  • eliminates MFA dispatch and per‑request closure allocation
  • reduces wrapper overhead for medium/large JSON bodies
  • maintains full Plug.Parsers compatibility and error semantics

Benchmarks show ~10% improvement on medium/large JSON payloads.

@josevalim

Copy link
Copy Markdown
Member

Unfortunately this pull request is fundamentally broken: the init can happen at compile-time, which means a different environment than the one that calls call. Also, if performance is being measured, please include benchmarks, code and methodology.

@josevalim josevalim closed this Jul 23, 2026
@saleyn

saleyn commented Jul 24, 2026

Copy link
Copy Markdown
Author

I could include the benchmark if you'd like, but if the init could be called at compile time, this is indeed a improper PR suggestion.

@josevalim

Copy link
Copy Markdown
Member

I pushed a fix that inlines some of the calls but yes, the approach here is definitely wrong, so no benchmarks needed for this PR. :)

@saleyn

saleyn commented Jul 24, 2026

Copy link
Copy Markdown
Author

I'll take a look at your change. This was another version I tried prior to making the PR, which was still slightly better than current, but slightly worse than the persistent_term cached approach:

defmodule Plug.Parsers.JSONLean do
  @moduledoc """
  Optimized JSON parser without persistent_term.
  """

  @behaviour Plug.Parsers

  # Accept both old and new Plug decoder formats
  def init(opts) do
    decoder =
      case Keyword.get(opts, :json_decoder, Jason) do
        mod when is_atom(mod) ->
          # Old Plug API: json_decoder: Jason
          &mod.decode!/1

        {mod, fun, extra} ->
          # MFA form: json_decoder: {Jason, :decode!, []}
          fn body -> apply(mod, fun, [body | extra]) end
      end

    %{decoder: decoder}
  end

  def parse(conn, "application", subtype, _headers, %{decoder: decoder}) do
    if subtype == "json" or String.ends_with?(subtype, "+json") do
      decode_body(conn, decoder)
    else
      {:next, conn}
    end
  end

  def parse(conn, _type, _subtype, _headers, _opts) do
    {:next, conn}
  end

  defp decode_body(conn, decoder) do
    case Plug.Conn.read_body(conn) do
      {:ok, body, conn} ->
        try do
          terms = decoder.(body)
          {:ok, normalize(terms), conn}
        rescue
          _ -> raise Plug.Parsers.ParseError
        end

      {:more, _, _} ->
        raise Plug.Parsers.ParseError
    end
  end

  # Match Plug behavior: wrap non-map JSON in "_json"
  defp normalize(%{} = map), do: map
  defp normalize(other), do: %{"_json" => other}
end

@saleyn

saleyn commented Jul 24, 2026

Copy link
Copy Markdown
Author

I did look at your change, and it indeed addresses the same concern. So all good :)

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