Current section
Files
Jump to
Current section
Files
lib/mix/tasks/vm_benchmark_json.ex
defmodule Mix.Tasks.GorillaStream.VmBenchmarkJson do
use Mix.Task
@shortdoc "Outputs JSON summary comparing Gorilla vs Gorilla+VictoriaMetrics preprocessing"
@moduledoc """
Produces a JSON summary for POC comparisons. You can write to a file or log to stdout via Logger.
Examples:
mix gorilla_stream.vm_benchmark_json --count 10000 --out vm_summary.json
mix gorilla_stream.vm_benchmark_json 20000
"""
require Logger
alias GorillaStream.Compression.Gorilla
@impl true
def run(args) do
Mix.Task.run("app.start")
{opts, positional, _} =
OptionParser.parse(args,
switches: [count: :integer, out: :string],
aliases: [n: :count, o: :out]
)
count =
cond do
is_integer(opts[:count]) ->
opts[:count]
match?([_ | _], positional) and integer_string?(hd(positional)) ->
String.to_integer(hd(positional))
true ->
10_000
end
out_path = opts[:out]
gauge = generate_gauge(count)
counter = generate_counter(count)
# Gauge baseline and VM
{g_t1, {:ok, g_bin1}} =
:timer.tc(fn -> Gorilla.compress(gauge, zlib: false, victoria_metrics: false) end)
g_size1 = byte_size(g_bin1)
{g_t2, {:ok, g_bin2}} =
:timer.tc(fn ->
Gorilla.compress(gauge,
zlib: false,
victoria_metrics: true,
is_counter: false,
scale_decimals: :auto
)
end)
g_size2 = byte_size(g_bin2)
g_orig = byte_size(:erlang.term_to_binary(gauge))
# Counter baseline and VM
{c_t1, {:ok, c_bin1}} =
:timer.tc(fn -> Gorilla.compress(counter, zlib: false, victoria_metrics: false) end)
c_size1 = byte_size(c_bin1)
{c_t2, {:ok, c_bin2}} =
:timer.tc(fn ->
Gorilla.compress(counter,
zlib: false,
victoria_metrics: true,
is_counter: true,
scale_decimals: :auto
)
end)
c_size2 = byte_size(c_bin2)
c_orig = byte_size(:erlang.term_to_binary(counter))
# Verify round-trips (will raise if failure)
{:ok, _} = Gorilla.decompress(g_bin2)
{:ok, _} = Gorilla.decompress(c_bin2)
summary = %{
count: count,
gauge: %{
original_size: g_orig,
baseline_size: g_size1,
vm_size: g_size2,
baseline_ratio: ratio(g_size1, g_orig),
vm_ratio: ratio(g_size2, g_orig),
baseline_encode_time_us: g_t1,
vm_encode_time_us: g_t2
},
counter: %{
original_size: c_orig,
baseline_size: c_size1,
vm_size: c_size2,
baseline_ratio: ratio(c_size1, c_orig),
vm_ratio: ratio(c_size2, c_orig),
baseline_encode_time_us: c_t1,
vm_encode_time_us: c_t2
}
}
json = encode_json(summary)
case out_path do
nil ->
Logger.info(json)
path ->
case File.write(path, json <> "\n") do
:ok -> Logger.info("Wrote JSON summary to #{path}")
{:error, reason} -> Logger.error("Failed to write JSON summary: #{inspect(reason)}")
end
end
end
defp integer_string?(s) when is_binary(s) do
case Integer.parse(s) do
{_, ""} -> true
_ -> false
end
end
defp ratio(sz, orig) when orig > 0, do: Float.round(sz / orig, 6)
defp ratio(_sz, _orig), do: 0.0
# Simple JSON encoder for maps/lists with numbers/strings/booleans (no external deps)
defp encode_json(value), do: encode_value(value)
defp encode_value(v) when is_map(v) do
entries =
v
|> Enum.map(fn {k, val} ->
key = to_string(k)
"\"#{escape(key)}\":#{encode_value(val)}"
end)
|> Enum.join(",")
"{" <> entries <> "}"
end
defp encode_value(v) when is_list(v) do
items = v |> Enum.map(&encode_value/1) |> Enum.join(",")
"[" <> items <> "]"
end
defp encode_value(v) when is_binary(v), do: "\"" <> escape(v) <> "\""
defp encode_value(v) when is_integer(v) or is_float(v), do: to_string(v)
defp encode_value(true), do: "true"
defp encode_value(false), do: "false"
defp encode_value(nil), do: "null"
defp escape(s) do
s
|> String.replace("\\", "\\\\")
|> String.replace("\"", "\\\"")
|> String.replace("\n", "\\n")
|> String.replace("\r", "\\r")
|> String.replace("\t", "\\t")
end
defp generate_gauge(n) do
base = 1_700_000_000
for i <- 0..(n - 1) do
{base + i, 100.0 + 0.01 * i + :math.sin(i / 50) * 0.1}
end
end
defp generate_counter(n) do
base = 1_700_000_000
increments = Stream.repeatedly(fn -> :rand.uniform(10) - 1 end) |> Enum.take(n)
{vals, _} =
Enum.map_reduce(increments, 1_000.0, fn inc, acc ->
v = acc + inc
{v, v}
end)
for {v, i} <- Enum.with_index(vals), do: {base + i, v + 0.01}
end
end