diff --git a/lib/kino/process.ex b/lib/kino/process.ex
index 5e97106b..e058f02f 100644
--- a/lib/kino/process.ex
+++ b/lib/kino/process.ex
@@ -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;
"""
@@ -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
@@ -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)
@@ -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)
@@ -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}
@@ -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}
#{pid_text}"
+ end
+
defp maybe_add_participant({participants, idx}, pid) when is_pid(pid) do
if Map.has_key?(participants, pid) do
{participants, idx}
@@ -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}
#{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
diff --git a/lib/kino/process/tracer.ex b/lib/kino/process/tracer.ex
index 77448229..eb277e69 100644
--- a/lib/kino/process/tracer.ex
+++ b/lib/kino/process/tracer.ex
@@ -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,
@@ -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
diff --git a/test/kino/process_test.exs b/test/kino/process_test.exs
index 2e660659..e6b32e68 100644
--- a/test/kino/process_test.exs
+++ b/test/kino/process_test.exs
@@ -114,6 +114,39 @@ 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(?.*)/, inspect(task))
+
+ assert diagram =~
+ "0(supervisor_parent):::root ---> 1(\"#{process_label}
#{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/,
@@ -121,6 +154,54 @@ defmodule Kino.ProcessTest do
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}
#{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}}