From f52a273bb2a72a94a1b225461d185cb00f585933 Mon Sep 17 00:00:00 2001 From: BennyFranciscus <268274351+BennyFranciscus@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:04:53 +0000 Subject: [PATCH 1/2] Add Phoenix: Elixir web framework on BEAM/Cowboy (first Elixir entry! ~23k stars) --- frameworks/phoenix/Dockerfile | 25 +++ frameworks/phoenix/README.md | 16 ++ frameworks/phoenix/config/config.exs | 11 + frameworks/phoenix/config/prod.exs | 7 + frameworks/phoenix/config/runtime.exs | 4 + .../lib/httparena_phoenix/application.ex | 16 ++ .../lib/httparena_phoenix/bench_controller.ex | 188 ++++++++++++++++++ .../lib/httparena_phoenix/data_loader.ex | 75 +++++++ .../phoenix/lib/httparena_phoenix/endpoint.ex | 5 + .../lib/httparena_phoenix/error_json.ex | 5 + .../phoenix/lib/httparena_phoenix/router.ex | 15 ++ frameworks/phoenix/meta.json | 19 ++ frameworks/phoenix/mix.exs | 29 +++ 13 files changed, 415 insertions(+) create mode 100644 frameworks/phoenix/Dockerfile create mode 100644 frameworks/phoenix/README.md create mode 100644 frameworks/phoenix/config/config.exs create mode 100644 frameworks/phoenix/config/prod.exs create mode 100644 frameworks/phoenix/config/runtime.exs create mode 100644 frameworks/phoenix/lib/httparena_phoenix/application.ex create mode 100644 frameworks/phoenix/lib/httparena_phoenix/bench_controller.ex create mode 100644 frameworks/phoenix/lib/httparena_phoenix/data_loader.ex create mode 100644 frameworks/phoenix/lib/httparena_phoenix/endpoint.ex create mode 100644 frameworks/phoenix/lib/httparena_phoenix/error_json.ex create mode 100644 frameworks/phoenix/lib/httparena_phoenix/router.ex create mode 100644 frameworks/phoenix/meta.json create mode 100644 frameworks/phoenix/mix.exs diff --git a/frameworks/phoenix/Dockerfile b/frameworks/phoenix/Dockerfile new file mode 100644 index 0000000..f1f5869 --- /dev/null +++ b/frameworks/phoenix/Dockerfile @@ -0,0 +1,25 @@ +FROM elixir:1.17-otp-27-alpine AS build +RUN apk add --no-cache gcc musl-dev make +WORKDIR /app +ENV MIX_ENV=prod + +# Install hex + rebar +RUN mix local.hex --force && mix local.rebar --force + +# Get deps +COPY mix.exs ./ +RUN mix deps.get --only prod +RUN mix deps.compile + +# Compile app +COPY config ./config +COPY lib ./lib +RUN mix compile +RUN mix release + +FROM erlang:27-alpine +RUN apk add --no-cache libstdc++ +WORKDIR /app +COPY --from=build /app/_build/prod/rel/httparena_phoenix /app +EXPOSE 8080 +CMD ["/app/bin/httparena_phoenix", "start"] diff --git a/frameworks/phoenix/README.md b/frameworks/phoenix/README.md new file mode 100644 index 0000000..3913692 --- /dev/null +++ b/frameworks/phoenix/README.md @@ -0,0 +1,16 @@ +# Phoenix — Elixir Web Framework + +[Phoenix](https://github.com/phoenixframework/phoenix) is the most popular Elixir web framework, built on the BEAM VM (Erlang's runtime). It leverages OTP's lightweight processes for massive concurrency and fault tolerance. + +## Stack + +- **Phoenix 1.7** on Cowboy HTTP server +- **BEAM VM** (Erlang/OTP 27) — preemptive scheduling, millions of lightweight processes +- **Jason** for JSON encoding/decoding +- **Exqlite** for SQLite access +- Pre-computed JSON + gzip caches via `:persistent_term` (global read-optimized storage) +- Mix release for production deployment + +## Why Phoenix? + +Phoenix is THE Elixir web framework — ~23k GitHub stars, used in production by companies like Discord (originally), Bleacher Report, PepsiCo. The BEAM VM's concurrency model (lightweight processes with preemptive scheduling) is fundamentally different from thread-based or async/await approaches. Interesting to see how it compares with gleam-mist (also BEAM) and traditional frameworks. diff --git a/frameworks/phoenix/config/config.exs b/frameworks/phoenix/config/config.exs new file mode 100644 index 0000000..a1abbd5 --- /dev/null +++ b/frameworks/phoenix/config/config.exs @@ -0,0 +1,11 @@ +import Config + +config :httparena_phoenix, HttparenaPhoenix.Endpoint, + http: [port: 8080, ip: {0, 0, 0, 0}, transport_options: [num_acceptors: 100]], + server: true, + render_errors: [formats: [json: HttparenaPhoenix.ErrorJSON]], + pubsub_server: HttparenaPhoenix.PubSub + +config :logger, level: :warning + +config :phoenix, :json_library, Jason diff --git a/frameworks/phoenix/config/prod.exs b/frameworks/phoenix/config/prod.exs new file mode 100644 index 0000000..591b57c --- /dev/null +++ b/frameworks/phoenix/config/prod.exs @@ -0,0 +1,7 @@ +import Config + +config :httparena_phoenix, HttparenaPhoenix.Endpoint, + http: [port: 8080, ip: {0, 0, 0, 0}, transport_options: [num_acceptors: 100]], + server: true + +config :logger, level: :warning diff --git a/frameworks/phoenix/config/runtime.exs b/frameworks/phoenix/config/runtime.exs new file mode 100644 index 0000000..a9d76a7 --- /dev/null +++ b/frameworks/phoenix/config/runtime.exs @@ -0,0 +1,4 @@ +import Config + +config :httparena_phoenix, HttparenaPhoenix.Endpoint, + server: true diff --git a/frameworks/phoenix/lib/httparena_phoenix/application.ex b/frameworks/phoenix/lib/httparena_phoenix/application.ex new file mode 100644 index 0000000..07dac0b --- /dev/null +++ b/frameworks/phoenix/lib/httparena_phoenix/application.ex @@ -0,0 +1,16 @@ +defmodule HttparenaPhoenix.Application do + use Application + + @impl true + def start(_type, _args) do + # Pre-load data + HttparenaPhoenix.DataLoader.load() + + children = [ + HttparenaPhoenix.Endpoint + ] + + opts = [strategy: :one_for_one, name: HttparenaPhoenix.Supervisor] + Supervisor.start_link(children, opts) + end +end diff --git a/frameworks/phoenix/lib/httparena_phoenix/bench_controller.ex b/frameworks/phoenix/lib/httparena_phoenix/bench_controller.ex new file mode 100644 index 0000000..704620b --- /dev/null +++ b/frameworks/phoenix/lib/httparena_phoenix/bench_controller.ex @@ -0,0 +1,188 @@ +defmodule HttparenaPhoenix.BenchController do + use Phoenix.Controller + + import Plug.Conn + + @db_query "SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count FROM items WHERE price BETWEEN ?1 AND ?2 LIMIT 50" + + def pipeline(conn, _params) do + conn + |> put_resp_header("server", "phoenix") + |> put_resp_header("content-type", "text/plain") + |> send_resp(200, "ok") + end + + def baseline11(conn, params) do + query_sum = sum_query_params(params) + + body_val = + case conn.method do + "POST" -> + {:ok, body, _conn} = read_body(conn) + case Integer.parse(String.trim(body)) do + {n, _} -> n + :error -> 0 + end + _ -> 0 + end + + total = query_sum + body_val + + conn + |> put_resp_header("server", "phoenix") + |> put_resp_header("content-type", "text/plain") + |> send_resp(200, Integer.to_string(total)) + end + + def baseline2(conn, params) do + total = sum_query_params(params) + + conn + |> put_resp_header("server", "phoenix") + |> put_resp_header("content-type", "text/plain") + |> send_resp(200, Integer.to_string(total)) + end + + def json(conn, _params) do + json_cache = :persistent_term.get(:json_cache) + + conn + |> put_resp_header("server", "phoenix") + |> put_resp_header("content-type", "application/json") + |> send_resp(200, json_cache) + end + + def compression(conn, _params) do + accepts_gzip = + case get_req_header(conn, "accept-encoding") do + [val | _] -> String.contains?(val, "gzip") + _ -> false + end + + if accepts_gzip do + gzip_cache = :persistent_term.get(:gzip_large_cache) + + conn + |> put_resp_header("server", "phoenix") + |> put_resp_header("content-encoding", "gzip") + |> put_resp_header("content-type", "application/json") + |> send_resp(200, gzip_cache) + else + json_large_cache = :persistent_term.get(:json_large_cache) + + conn + |> put_resp_header("server", "phoenix") + |> put_resp_header("content-type", "application/json") + |> send_resp(200, json_large_cache) + end + end + + def upload(conn, _params) do + {:ok, body, conn} = read_body(conn, length: 25_000_000) + size = byte_size(body) + + conn + |> put_resp_header("server", "phoenix") + |> put_resp_header("content-type", "text/plain") + |> send_resp(200, Integer.to_string(size)) + end + + def db(conn, params) do + db_available = :persistent_term.get(:db_available) + + unless db_available do + conn + |> put_resp_header("server", "phoenix") + |> put_resp_header("content-type", "application/json") + |> send_resp(200, ~s({"items":[],"count":0})) + else + min_val = parse_float(params["min"], 10.0) + max_val = parse_float(params["max"], 50.0) + + {:ok, db_conn} = Exqlite.Sqlite3.open("/data/benchmark.db", [:readonly]) + :ok = Exqlite.Sqlite3.execute(db_conn, "PRAGMA mmap_size=268435456") + {:ok, stmt} = Exqlite.Sqlite3.prepare(db_conn, @db_query) + :ok = Exqlite.Sqlite3.bind(stmt, [min_val, max_val]) + + rows = fetch_all_rows(db_conn, stmt, []) + :ok = Exqlite.Sqlite3.release(db_conn, stmt) + :ok = Exqlite.Sqlite3.close(db_conn) + + items = Enum.map(rows, fn [id, name, category, price, quantity, active, tags_str, rating_score, rating_count] -> + tags = case Jason.decode(tags_str) do + {:ok, t} when is_list(t) -> t + _ -> [] + end + + %{ + "id" => id, + "name" => name, + "category" => category, + "price" => price, + "quantity" => quantity, + "active" => active != 0, + "tags" => tags, + "rating" => %{"score" => rating_score, "count" => rating_count} + } + end) + + body = Jason.encode!(%{"items" => items, "count" => length(items)}) + + conn + |> put_resp_header("server", "phoenix") + |> put_resp_header("content-type", "application/json") + |> send_resp(200, body) + end + end + + def static_file(conn, %{"filename" => filename}) do + static_files = :persistent_term.get(:static_files) + + case Map.get(static_files, filename) do + nil -> + conn + |> put_resp_header("server", "phoenix") + |> send_resp(404, "Not Found") + + %{data: data, content_type: ct} -> + conn + |> put_resp_header("server", "phoenix") + |> put_resp_header("content-type", ct) + |> send_resp(200, data) + end + end + + # Helpers + + defp sum_query_params(params) do + params + |> Enum.reduce(0, fn + {"filename", _v}, acc -> acc + {_k, v}, acc -> + case Integer.parse(v) do + {n, ""} -> acc + n + _ -> acc + end + end) + end + + defp parse_float(nil, default), do: default + defp parse_float(val, default) when is_binary(val) do + case Float.parse(val) do + {f, _} -> f + :error -> + case Integer.parse(val) do + {i, _} -> i * 1.0 + :error -> default + end + end + end + defp parse_float(_, default), do: default + + defp fetch_all_rows(db_conn, stmt, acc) do + case Exqlite.Sqlite3.step(db_conn, stmt) do + {:row, row} -> fetch_all_rows(db_conn, stmt, [row | acc]) + :done -> Enum.reverse(acc) + end + end +end diff --git a/frameworks/phoenix/lib/httparena_phoenix/data_loader.ex b/frameworks/phoenix/lib/httparena_phoenix/data_loader.ex new file mode 100644 index 0000000..2f3692c --- /dev/null +++ b/frameworks/phoenix/lib/httparena_phoenix/data_loader.ex @@ -0,0 +1,75 @@ +defmodule HttparenaPhoenix.DataLoader do + @moduledoc """ + Pre-loads and caches dataset, large dataset, static files, and pre-computed JSON/gzip responses. + """ + + def load do + dataset_path = System.get_env("DATASET_PATH") || "/data/dataset.json" + + dataset = load_json(dataset_path) + large_dataset = load_json("/data/dataset-large.json") + + json_cache = build_json_response(dataset) + json_large_cache = build_json_response(large_dataset) + gzip_large_cache = :zlib.gzip(json_large_cache) + + static_files = load_static_files() + + db_available = File.exists?("/data/benchmark.db") + + :persistent_term.put(:dataset, dataset) + :persistent_term.put(:json_cache, json_cache) + :persistent_term.put(:json_large_cache, json_large_cache) + :persistent_term.put(:gzip_large_cache, gzip_large_cache) + :persistent_term.put(:static_files, static_files) + :persistent_term.put(:db_available, db_available) + end + + defp load_json(path) do + case File.read(path) do + {:ok, data} -> + case Jason.decode(data) do + {:ok, items} when is_list(items) -> items + _ -> [] + end + _ -> [] + end + end + + defp build_json_response(dataset) do + items = Enum.map(dataset, fn d -> + total = Float.round(d["price"] * d["quantity"] * 1.0, 2) + Map.put(d, "total", total) + end) + + Jason.encode!(%{"items" => items, "count" => length(items)}) + end + + defp load_static_files do + case File.ls("/data/static") do + {:ok, entries} -> + Enum.reduce(entries, %{}, fn name, acc -> + path = "/data/static/#{name}" + case File.read(path) do + {:ok, data} -> + Map.put(acc, name, %{data: data, content_type: get_mime(name)}) + _ -> acc + end + end) + _ -> %{} + end + end + + defp get_mime(filename) do + cond do + String.ends_with?(filename, ".css") -> "text/css" + String.ends_with?(filename, ".js") -> "application/javascript" + String.ends_with?(filename, ".html") -> "text/html" + String.ends_with?(filename, ".woff2") -> "font/woff2" + String.ends_with?(filename, ".svg") -> "image/svg+xml" + String.ends_with?(filename, ".webp") -> "image/webp" + String.ends_with?(filename, ".json") -> "application/json" + true -> "application/octet-stream" + end + end +end diff --git a/frameworks/phoenix/lib/httparena_phoenix/endpoint.ex b/frameworks/phoenix/lib/httparena_phoenix/endpoint.ex new file mode 100644 index 0000000..a21ec14 --- /dev/null +++ b/frameworks/phoenix/lib/httparena_phoenix/endpoint.ex @@ -0,0 +1,5 @@ +defmodule HttparenaPhoenix.Endpoint do + use Phoenix.Endpoint, otp_app: :httparena_phoenix + + plug HttparenaPhoenix.Router +end diff --git a/frameworks/phoenix/lib/httparena_phoenix/error_json.ex b/frameworks/phoenix/lib/httparena_phoenix/error_json.ex new file mode 100644 index 0000000..fd58469 --- /dev/null +++ b/frameworks/phoenix/lib/httparena_phoenix/error_json.ex @@ -0,0 +1,5 @@ +defmodule HttparenaPhoenix.ErrorJSON do + def render(template, _assigns) do + %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}} + end +end diff --git a/frameworks/phoenix/lib/httparena_phoenix/router.ex b/frameworks/phoenix/lib/httparena_phoenix/router.ex new file mode 100644 index 0000000..cb56dba --- /dev/null +++ b/frameworks/phoenix/lib/httparena_phoenix/router.ex @@ -0,0 +1,15 @@ +defmodule HttparenaPhoenix.Router do + use Phoenix.Router + + scope "/", HttparenaPhoenix do + get "/pipeline", BenchController, :pipeline + get "/baseline11", BenchController, :baseline11 + post "/baseline11", BenchController, :baseline11 + get "/baseline2", BenchController, :baseline2 + get "/json", BenchController, :json + get "/compression", BenchController, :compression + get "/db", BenchController, :db + post "/upload", BenchController, :upload + get "/static/:filename", BenchController, :static_file + end +end diff --git a/frameworks/phoenix/meta.json b/frameworks/phoenix/meta.json new file mode 100644 index 0000000..0168c5c --- /dev/null +++ b/frameworks/phoenix/meta.json @@ -0,0 +1,19 @@ +{ + "display_name": "phoenix", + "language": "Elixir", + "type": "framework", + "engine": "BEAM (Erlang VM)", + "description": "Phoenix 1.7 on Cowboy — the most popular Elixir web framework. First Elixir entry in HttpArena.", + "repo": "https://github.com/phoenixframework/phoenix", + "enabled": true, + "tests": [ + "baseline", + "noisy", + "pipelined", + "limited-conn", + "json", + "upload", + "compression", + "mixed" + ] +} diff --git a/frameworks/phoenix/mix.exs b/frameworks/phoenix/mix.exs new file mode 100644 index 0000000..148716e --- /dev/null +++ b/frameworks/phoenix/mix.exs @@ -0,0 +1,29 @@ +defmodule HttparenaPhoenix.MixProject do + use Mix.Project + + def project do + [ + app: :httparena_phoenix, + version: "0.1.0", + elixir: "~> 1.17", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + def application do + [ + extra_applications: [:logger], + mod: {HttparenaPhoenix.Application, []} + ] + end + + defp deps do + [ + {:phoenix, "~> 1.7"}, + {:plug_cowboy, "~> 2.7"}, + {:jason, "~> 1.4"}, + {:exqlite, "~> 0.27"} + ] + end +end From 19caa9cfe9bbf56b265d0cb6d272284a7c7d52bb Mon Sep 17 00:00:00 2001 From: BennyFranciscus <268274351+BennyFranciscus@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:07:41 +0000 Subject: [PATCH 2/2] Fix: add fetch_query_params plug to router pipeline Phoenix router doesn't auto-merge query params into controller params without either Plug.Parsers or explicit fetch_query_params plug. --- frameworks/phoenix/lib/httparena_phoenix/router.ex | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frameworks/phoenix/lib/httparena_phoenix/router.ex b/frameworks/phoenix/lib/httparena_phoenix/router.ex index cb56dba..7e11f89 100644 --- a/frameworks/phoenix/lib/httparena_phoenix/router.ex +++ b/frameworks/phoenix/lib/httparena_phoenix/router.ex @@ -1,7 +1,13 @@ defmodule HttparenaPhoenix.Router do use Phoenix.Router + pipeline :bench do + plug :fetch_query_params + end + scope "/", HttparenaPhoenix do + pipe_through :bench + get "/pipeline", BenchController, :pipeline get "/baseline11", BenchController, :baseline11 post "/baseline11", BenchController, :baseline11