Skip to content

Commit

Permalink
Support for OTP 27 process labels (#442)
Browse files Browse the repository at this point in the history
  • Loading branch information
hugobarauna authored Jun 13, 2024
1 parent 357ce80 commit e587af3
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 18 deletions.
65 changes: 54 additions & 11 deletions lib/kino/process.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ defmodule Kino.Process do
alias Kino.Process.Tracer

@mermaid_classdefs """
classDef root fill:#c4b5fd, stroke:#374151, stroke-width:4px;
classDef supervisor fill:#c4b5fd, stroke:#374151, stroke-width:1px;
classDef worker fill:#66c2a5, stroke:#374151, stroke-width:1px;
classDef notstarted color:#777, fill:#d9d9d9, stroke:#777, stroke-width:1px;
classDef root fill:#c4b5fd, stroke:#374151, stroke-width:4px, line-height:1.5em;
classDef supervisor fill:#c4b5fd, stroke:#374151, stroke-width:1px, line-height:1.5em;
classDef worker fill:#66c2a5, stroke:#374151, stroke-width:1px, line-height:1.5em;
classDef notstarted color:#777, fill:#d9d9d9, stroke:#777, stroke-width:1px, line-height:1.5em;
classDef ets fill:#a5f3fc, stroke:#374151, stroke-width:1px;
"""

Expand Down Expand Up @@ -324,7 +324,7 @@ defmodule Kino.Process do
previous_tracer = :seq_trace.set_system_tracer(tracer_pid)

# Run the user supplied function and capture the events if no errors were encountered
{raw_trace_events, func_result} =
{%{raw_trace_events: raw_trace_events, process_labels: process_labels}, func_result} =
try do
func_result =
try do
Expand All @@ -336,7 +336,7 @@ defmodule Kino.Process do
:seq_trace.reset_trace()
end

{Tracer.get_trace_events(tracer_pid), func_result}
{Tracer.get_trace_info(tracer_pid), func_result}
after
# The Tracer GenServer is no longer needed, shut it down
GenServer.stop(tracer_pid)
Expand Down Expand Up @@ -380,7 +380,8 @@ defmodule Kino.Process do
if pid == calling_pid do
"participant #{idx} AS self();"
else
generate_participant_entry(pid, idx)
process_label = Map.get(process_labels, pid, :undefined)
generate_participant_entry(pid, idx, process_label)
end
end)

Expand Down Expand Up @@ -413,6 +414,7 @@ defmodule Kino.Process do

sequence_diagram =
Mermaid.new("""
%%{init: {'themeCSS': '.actor:last-of-type:not(:only-of-type) {dominant-baseline: hanging;}'} }%%
sequenceDiagram
#{participants}
#{messages}
Expand All @@ -421,15 +423,36 @@ defmodule Kino.Process do
{func_result, sequence_diagram}
end

defp generate_participant_entry(pid, idx) do
# TODO: use :proc_lib.get_label/1 once we require OTP 27
if Code.ensure_loaded?(:proc_lib) and function_exported?(:proc_lib, :get_label, 1) do
defp get_label(pid), do: :proc_lib.get_label(pid)
else
defp get_label(_pid), do: :undefined
end

defp generate_participant_entry(pid, idx, process_label) do
try do
{:registered_name, name} = process_info(pid, :registered_name)
"participant #{idx} AS #{module_or_atom_to_string(name)};"
rescue
_ -> "participant #{idx} AS #35;PID#{:erlang.pid_to_list(pid)};"
_ ->
case process_label do
:undefined ->
"participant #{idx} AS #35;PID#{:erlang.pid_to_list(pid)};"

process_label ->
"participant #{idx} AS #{format_for_mermaid_participant_alias(pid, process_label)};"
end
end
end

defp format_for_mermaid_participant_alias(pid, process_label) do
pid_text = :erlang.pid_to_list(pid) |> List.to_string()

label = process_label |> inspect() |> String.replace(~s{"}, "")
"#{label}<br/>#{pid_text}"
end

defp maybe_add_participant({participants, idx}, pid) when is_pid(pid) do
if Map.has_key?(participants, pid) do
{participants, idx}
Expand Down Expand Up @@ -740,13 +763,33 @@ defmodule Kino.Process do

display =
case process_info(pid, :registered_name) do
{:registered_name, []} -> inspect(pid)
{:registered_name, name} -> module_or_atom_to_string(name)
{:registered_name, []} ->
case get_label(pid) do
:undefined -> inspect(pid)
process_label -> format_for_mermaid_graph_node(pid, process_label)
end

{:registered_name, name} ->
module_or_atom_to_string(name)
end

"#{idx}(#{display}):::#{type}"
end

defp format_for_mermaid_graph_node(pid, process_label) do
pid_text = :erlang.pid_to_list(pid) |> List.to_string()

label = process_label |> inspect() |> String.replace(~s{"}, "")

format_as_mermaid_unicode_text("#{label}<br/>#{pid_text}")
end

# this is needed to use unicode inside node's text
# (https://mermaid.js.org/syntax/flowchart.html#unicode-text)
defp format_as_mermaid_unicode_text(node_text) do
"\"#{node_text}\""
end

defp module_or_atom_to_string(atom) do
case Atom.to_string(atom) do
"Elixir." <> rest -> rest
Expand Down
32 changes: 25 additions & 7 deletions lib/kino/process/tracer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,22 @@ defmodule Kino.Process.Tracer do
GenServer.start_link(__MODULE__, nil)
end

def get_trace_events(tracer) do
GenServer.call(tracer, :get_trace_events)
def get_trace_info(tracer) do
GenServer.call(tracer, :get_trace_info)
end

@impl true
def init(_) do
{:ok, []}
{:ok, %{raw_trace_events: [], process_labels: %{}}}
end

@impl true
def handle_call(:get_trace_events, _from, trace_events) do
{:reply, trace_events, trace_events}
def handle_call(:get_trace_info, _from, trace_info) do
{:reply, trace_info, trace_info}
end

@impl true
def handle_info({:seq_trace, _, {:send, _, from, to, message}, timestamp}, trace_events) do
def handle_info({:seq_trace, _, {:send, _, from, to, message}, timestamp}, trace_info) do
new_event = %{
type: :send,
timestamp: timestamp,
Expand All @@ -31,10 +31,28 @@ defmodule Kino.Process.Tracer do
message: message
}

{:noreply, [new_event | trace_events]}
trace_events = [new_event | trace_info.raw_trace_events]

process_labels =
trace_info.process_labels
|> put_new_label(from)
|> put_new_label(to)

{:noreply, %{trace_info | raw_trace_events: trace_events, process_labels: process_labels}}
end

def handle_info(_ignored_event, trace_events) do
{:noreply, trace_events}
end

defp put_new_label(process_labels, pid) do
Map.put_new_lazy(process_labels, pid, fn -> get_label(pid) end)
end

# :proc_lib.get_label/1 was added in OTP 27
if Code.ensure_loaded?(:proc_lib) and function_exported?(:proc_lib, :get_label, 1) do
defp get_label(pid), do: :proc_lib.get_label(pid)
else
defp get_label(_pid), do: :undefined
end
end
81 changes: 81 additions & 0 deletions test/kino/process_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -114,13 +114,94 @@ defmodule Kino.ProcessTest do
assert content =~ "0(supervisor_parent):::root ---> 2(#{inspect(agent)}):::worker"
end

# TODO: remove once we require Elixir v1.17.0
if function_exported?(Process, :set_label, 1) do
test "uses process label in the diagram to identify a process" do
process_label = "my task"

supervisor =
start_supervised!(%{
id: Supervisor,
start:
{Supervisor, :start_link,
[
[
{Task,
fn ->
Process.set_label(process_label)
Process.sleep(:infinity)
end}
],
[name: :supervisor_parent, strategy: :one_for_one]
]}
})

[{_, task, _, _}] = Supervisor.which_children(supervisor)

diagram = Kino.Process.sup_tree(supervisor) |> mermaid()

%{"pid" => pid_text} = Regex.named_captures(~r/#PID(?<pid>.*)/, inspect(task))

assert diagram =~
"0(supervisor_parent):::root ---> 1(\"#{process_label}<br/>#{pid_text}\"):::worker"
end
end

test "raises if supervisor does not exist" do
assert_raise ArgumentError,
~r/the provided identifier :not_a_valid_supervisor does not reference a running process/,
fn -> Kino.Process.sup_tree(:not_a_valid_supervisor) end
end
end

describe "seq_trace/2" do
# Process.set_label/1 was addeed in Elixir 1.17.0
if function_exported?(Process, :set_label, 1) do
test "uses process label to identify a process" do
process_label = "ponger"
ponger = start_supervised!({Kino.ProcessTest.Ponger, [label: process_label]})

traced_function = fn ->
send(ponger, {:ping, self()})

receive do
:pong -> :ponged!
end
end

{_func_result, diagram} = Kino.Process.seq_trace(traced_function)
diagram = mermaid(diagram)

ponger_pid = :erlang.pid_to_list(ponger) |> List.to_string()
assert diagram =~ ~r/participant 1 AS #{process_label}<br\/>#{ponger_pid};/
end
end
end

defmodule Ponger do
use GenServer

def start_link(opts) do
GenServer.start_link(__MODULE__, opts)
end

# Process.set_label/1 was addeed in Elixir 1.17.0
@compile {:no_warn_undefined, {Process, :set_label, 1}}
@impl true
def init(opts) do
Process.set_label(opts[:label])

{:ok, nil}
end

@impl true
def handle_info({:ping, from}, state) do
send(from, :pong)

{:noreply, state}
end
end

defp mermaid(%Kino.JS{ref: ref}) do
send(Kino.JS.DataStore, {:connect, self(), %{origin: "client:#{inspect(self())}", ref: ref}})
assert_receive {:connect_reply, data, %{ref: ^ref}}
Expand Down

0 comments on commit e587af3

Please sign in to comment.