defmodule FlameOn.SVG do
use Phoenix.LiveComponent
alias FlameOn.Capture.Block
def render(assigns) do
%Block{} = top_block = Map.fetch!(assigns, :block)
assigns =
assigns
|> assign(:blocks, List.flatten(flatten(top_block)))
|> assign(:duration_ratio, 1276 / top_block.duration)
|> assign(:block_height, 25)
|> assign(:top_block, top_block)
rendered =
~H"""
<svg width="1276" height={@block_height * @top_block.max_child_level} style="background-color: white;" xmlns="http://www.w3.org/2000/svg">
<style nonce={get_in(@csp_nonces, [:style])}>
svg > svg {
cursor: pointer;
}
svg > svg > rect {
stroke: white;
rx: 5px;
}
svg > svg > text {
font-size: <%= @block_height / 2 %>px;
font-family: monospace;
dominant-baseline: middle;
}
</style>
<%= for block <- @blocks do %>
{render_flame_on_block(%{
block: block,
block_height: @block_height,
duration_ratio: @duration_ratio,
top_block: @top_block,
parent: @parent,
socket: @socket
})}
<% end %>
</svg>
"""
html =
rendered
|> Phoenix.HTML.Safe.to_iodata()
|> IO.iodata_to_binary()
send_update(assigns.parent, html_render: html)
rendered
end
defp render_flame_on_block(%{block: %Block{function: nil}}), do: ""
defp render_flame_on_block(assigns) do
# Don't bother rendering a block if it's shorter than 0.1% of the top block duration,
# since we won't even be able to see it
~H"""
<%= if @block.duration / @top_block.duration > 0.001 do %>
<svg
width={Enum.max([trunc(@block.duration * @duration_ratio), 1])}
height={@block_height}
x={(@block.absolute_start - @top_block.absolute_start) * @duration_ratio}
y={(@block.level - @top_block.level) * @block_height}
phx-click="view_block"
phx-target={@parent}
phx-value-id={@block.id}
>
<rect width="100%" height="100%" fill={color_for_function(@block.function)}></rect>
<text x={@block_height / 4} y={@block_height * 0.5}>{mfa_to_string(@block.function)}</text>
<title>
{format_integer(@block.duration)}µs ({trunc(@block.duration * 100 / @top_block.duration)}%) {mfa_to_string(@block.function)}
</title>
</svg>
<% end %>
"""
end
defp flatten(%Block{children: children} = block) do
[block | Enum.map(children, &flatten/1)]
end
defp color_for_function({module, _, _}) do
case "#{module}" do
"Elixir." <> rest -> rest
other -> other
end
|> String.split(".")
|> hd()
|> color_for_module()
end
defp color_for_function(:sleep) do
color_for_module("sleep")
end
defp color_for_module(module) do
red = :erlang.phash2(module <> "red", 180) |> Kernel.+(75) |> Integer.to_string(16)
green = :erlang.phash2(module <> "green", 180) |> Kernel.+(75) |> Integer.to_string(16)
blue = :erlang.phash2(module <> "blue", 180) |> Kernel.+(75) |> Integer.to_string(16)
"\##{pad(red)}#{pad(green)}#{pad(blue)}"
end
defp pad(str) do
if String.length(str) == 1 do
"0" <> str
else
str
end
end
defp format_integer(integer) do
integer
|> Integer.to_charlist()
|> Enum.reverse()
|> Enum.chunk_every(3)
|> Enum.join(",")
|> String.reverse()
end
def mfa_to_string({m, f, a}) do
m =
case "#{m}" do
"Elixir." <> rest -> rest
other -> other
end
"#{m}.#{f}/#{a}"
end
def mfa_to_string(mfa) do
inspect(mfa)
end
end