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}}