Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for OTP 27 process labels #442

Merged
merged 16 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()

participant_alias = "#{inspect(process_label)}<br/>#{pid_text}"
String.replace(participant_alias, "\"", "")
jonatanklosko marked this conversation as resolved.
Show resolved Hide resolved
hugobarauna marked this conversation as resolved.
Show resolved Hide resolved
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()

"#{inspect(process_label)}<br/>#{pid_text}"
|> String.replace("\"", "")
|> format_as_mermaid_unicode_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(process_labels, pid, get_label(pid))
hugobarauna marked this conversation as resolved.
Show resolved Hide resolved
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
hugobarauna marked this conversation as resolved.
Show resolved Hide resolved
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
Loading