Skip to content

Optimize Cookies.encode#1315

Merged
josevalim merged 1 commit into
elixir-plug:mainfrom
preciz:optimization99
Jun 15, 2026
Merged

Optimize Cookies.encode#1315
josevalim merged 1 commit into
elixir-plug:mainfrom
preciz:optimization99

Conversation

@preciz

@preciz preciz commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Assisted by: Antigravity CLI : Gemini 3.5 Flash

I like the code structure of this pull request.

Bench:

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

defmodule OldCookies do
  def encode(key, opts \\ %{}) when is_map(opts) do
    value = Map.get(opts, :value)
    path = Map.get(opts, :path, "/")

    IO.iodata_to_binary([
      "#{key}=#{value}; path=#{path}",
      emit_if(opts[:domain], &["; domain=", &1]),
      emit_if(opts[:max_age], &encode_max_age(&1, opts)),
      emit_if(Map.get(opts, :secure, false), "; secure"),
      emit_if(Map.get(opts, :http_only, true), "; HttpOnly"),
      emit_if(Map.get(opts, :same_site, nil), &encode_same_site/1),
      emit_if(opts[:extra], &["; ", &1])
    ])
  end

  defp encode_max_age(max_age, opts) do
    time = Map.get(opts, :universal_time) || :calendar.universal_time()
    time = add_seconds(time, max_age)
    ["; expires=", rfc2822(time), "; max-age=", Integer.to_string(max_age)]
  end

  defp encode_same_site(value) when is_binary(value), do: "; SameSite=#{value}"

  defp emit_if(value, fun_or_string) do
    cond do
      !value ->
        []

      is_function(fun_or_string) ->
        fun_or_string.(value)

      is_binary(fun_or_string) ->
        fun_or_string
    end
  end

  defp pad(number) when number in 0..9, do: <<?0, ?0 + number>>
  defp pad(number), do: Integer.to_string(number)

  defp rfc2822({{year, month, day} = date, {hour, minute, second}}) do
    [
      weekday_name(:calendar.day_of_the_week(date)),
      ?,,
      ?\s,
      pad(day),
      ?\s,
      month_name(month),
      ?\s,
      Integer.to_string(year),
      ?\s,
      pad(hour),
      ?:,
      pad(minute),
      ?:,
      pad(second),
      " GMT"
    ]
  end

  defp weekday_name(1), do: "Mon"
  defp weekday_name(2), do: "Tue"
  defp weekday_name(3), do: "Wed"
  defp weekday_name(4), do: "Thu"
  defp weekday_name(5), do: "Fri"
  defp weekday_name(6), do: "Sat"
  defp weekday_name(7), do: "Sun"

  defp month_name(1), do: "Jan"
  defp month_name(2), do: "Feb"
  defp month_name(3), do: "Mar"
  defp month_name(4), do: "Apr"
  defp month_name(5), do: "May"
  defp month_name(6), do: "Jun"
  defp month_name(7), do: "Jul"
  defp month_name(8), do: "Aug"
  defp month_name(9), do: "Sep"
  defp month_name(10), do: "Oct"
  defp month_name(11), do: "Nov"
  defp month_name(12), do: "Dec"

  defp add_seconds(time, seconds_to_add) do
    time_seconds = :calendar.datetime_to_gregorian_seconds(time)
    :calendar.gregorian_seconds_to_datetime(time_seconds + seconds_to_add)
  end
end

defmodule NewCookies do
  def encode(key, opts \\ %{}) when is_map(opts) do
    value = Map.get(opts, :value)
    path = Map.get(opts, :path, "/")

    key = to_string(key)
    value = to_string(value)
    path = to_string(path)

    acc = [key, ?=, value, "; path=", path]
    acc = if domain = opts[:domain], do: [acc, "; domain=", domain], else: acc
    acc = if max_age = opts[:max_age], do: [acc | encode_max_age(max_age, opts)], else: acc
    acc = if Map.get(opts, :secure, false), do: [acc | "; secure"], else: acc
    acc = if Map.get(opts, :http_only, true), do: [acc | "; HttpOnly"], else: acc
    acc = if same_site = Map.get(opts, :same_site), do: [acc | encode_same_site(same_site)], else: acc
    acc = if extra = opts[:extra], do: [acc, "; ", extra], else: acc

    IO.iodata_to_binary(acc)
  end

  defp encode_max_age(max_age, opts) do
    time = Map.get(opts, :universal_time) || :calendar.universal_time()
    time = add_seconds(time, max_age)
    ["; expires=", rfc2822(time), "; max-age=", Integer.to_string(max_age)]
  end

  defp encode_same_site(value) when is_binary(value), do: ["; SameSite=", value]

  defp pad(n) when n < 10, do: <<?0, ?0 + n>>
  defp pad(n), do: <<?0 + div(n, 10), ?0 + rem(n, 10)>>

  defp rfc2822({{year, month, day} = date, {hour, minute, second}}) do
    [
      weekday_name(:calendar.day_of_the_week(date)),
      ?,,
      ?\s,
      pad(day),
      ?\s,
      month_name(month),
      ?\s,
      Integer.to_string(year),
      ?\s,
      pad(hour),
      ?:,
      pad(minute),
      ?:,
      pad(second),
      " GMT"
    ]
  end

  defp weekday_name(1), do: "Mon"
  defp weekday_name(2), do: "Tue"
  defp weekday_name(3), do: "Wed"
  defp weekday_name(4), do: "Thu"
  defp weekday_name(5), do: "Fri"
  defp weekday_name(6), do: "Sat"
  defp weekday_name(7), do: "Sun"

  defp month_name(1), do: "Jan"
  defp month_name(2), do: "Feb"
  defp month_name(3), do: "Mar"
  defp month_name(4), do: "Apr"
  defp month_name(5), do: "May"
  defp month_name(6), do: "Jun"
  defp month_name(7), do: "Jul"
  defp month_name(8), do: "Aug"
  defp month_name(9), do: "Sep"
  defp month_name(10), do: "Oct"
  defp month_name(11), do: "Nov"
  defp month_name(12), do: "Dec"

  defp add_seconds(time, seconds_to_add) do
    time_seconds = :calendar.datetime_to_gregorian_seconds(time)
    :calendar.gregorian_seconds_to_datetime(time_seconds + seconds_to_add)
  end
end

inputs = %{
  "Basic cookie" => {"foo", %{value: "bar"}},
  "Full options" => {"foo", %{value: "bar", path: "/baz", domain: "google.com", secure: true, same_site: "Lax", max_age: 60, universal_time: {{2012, 1, 7}, {15, 32, 10}}}},
  "Extra options" => {"foo", %{value: "bar", extra: "SameSite=Lax"}}
}

Benchee.run(
  %{
    "Old encode" => fn {key, opts} -> OldCookies.encode(key, opts) end,
    "New encode" => fn {key, opts} -> NewCookies.encode(key, opts) end
  },
  inputs: inputs,
  time: 2,
  warmup: 1,
  memory_time: 2
)

Results on noisy system:

Operating System: Linux
CPU Information: AMD Ryzen 7 8845HS w
Number of Available Cores: 16
Available memory: 54.72 GB
Elixir 1.20.0
Erlang 29.0.1
JIT enabled: true

Benchmark suite executing with the following configuration:
warmup: 1 s
time: 2 s
memory time: 2 s
reduction time: 0 ns
parallel: 1
inputs: Basic cookie, Extra options, Full options
Estimated total run time: 30 s
Excluding outliers: false

##### With input Basic cookie #####
Name                 ips        average  deviation         median         99th %
New encode        7.26 M      137.70 ns  ±4224.40%         110 ns         231 ns
Old encode        4.13 M      242.31 ns  ±2428.06%         161 ns         431 ns

Comparison:
New encode        7.26 M
Old encode        4.13 M - 1.76x slower +104.61 ns

Memory usage statistics:

Name          Memory usage
New encode           144 B
Old encode           248 B - 1.72x memory usage +104 B

**All measurements for memory usage were the same**

##### With input Extra options #####
Name                 ips        average  deviation         median         99th %
New encode        6.93 M      144.29 ns  ±3214.37%         130 ns         260 ns
Old encode        4.26 M      234.91 ns  ±2437.41%         180 ns         411 ns

Comparison:
New encode        6.93 M
Old encode        4.26 M - 1.63x slower +90.62 ns

Memory usage statistics:

Name          Memory usage
New encode           200 B
Old encode           288 B - 1.44x memory usage +88 B

**All measurements for memory usage were the same**

##### With input Full options #####
Name                 ips        average  deviation         median         99th %
New encode        1.90 M      525.63 ns  ±1051.00%         461 ns         891 ns
Old encode        1.47 M      681.61 ns  ±1213.50%         541 ns        1132 ns

Comparison:
New encode        1.90 M
Old encode        1.47 M - 1.30x slower +155.98 ns

Memory usage statistics:

Name          Memory usage
New encode           808 B
Old encode           848 B - 1.05x memory usage +40 B

**All measurements for memory usage were the same**

@josevalim josevalim merged commit 6ed9dc7 into elixir-plug:main Jun 15, 2026
2 checks passed
@josevalim

Copy link
Copy Markdown
Member

💚 💙 💜 💛 ❤️

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