dch

dch / Sup - visualise OTP supervision trees

Last active 5 hours ago

Like 0

sup is a fun elixir script to visualise OTP supervision trees in real time over a browser. Download, chmod and run it. Or just watch the video for funsies.

dch revised this gist 5 hours ago · 6003ee5

1 file changed, 3 insertions, 2 deletions

sup
@@ -231,9 +231,10 @@ case Supervisor.start_link(
231 231 {:ok, root} ->
232 232 # the process evaluating this file exits before the shell starts; don't take Root down with it
233 233 Process.unlink(root)
234 - IO.puts("Supervision tree live at http://localhost:#{port}")
234 + IO.puts("Supervision tree live at http://localhost:4000/")
235 235
236 236 {:error, reason} ->
237 - IO.puts(:stderr, "could not start on port #{port} (already in use?): #{inspect(reason)}")
237 + IO.puts(:stderr, "could not start on port 4000 (already in use?): #{inspect(reason)}")
238 238 System.halt(1)
239 239 end
240 +

dch revised this gist 16 hours ago · cd225fd

2 files changed, 239 insertions

sup (file created)
@@ -0,0 +1,239 @@
1 + #!/usr/bin/env iex
2 + # then open http://localhost:<port> and poke at it from the console:
3 + # Sup.tree(), Sup.graph(), Supervisor.which_children(Root)
4 + # vim: ft=elixir
5 + Mix.install([{:phoenix, "~> 1.8"}, {:bandit, "~> 1.5"}, {:jason, "~> 1.4"}])
6 + # ignore the intentional crash reports
7 + Logger.configure(level: :none)
8 +
9 + Application.put_env(:supviz, Supviz.Endpoint,
10 + adapter: Bandit.PhoenixAdapter,
11 + server: true,
12 + secret_key_base: Base.encode64(:crypto.strong_rand_bytes(64)),
13 + http: [port: 4000, thousand_island_options: [num_acceptors: 8]]
14 + )
15 +
16 + # walk the live supervision tree from Root, & every link & monitor between its processes
17 + defmodule Sup do
18 + def tree,
19 + do:
20 + (case Process.whereis(Root) do
21 + nil -> []
22 + p -> walk(p, nil, :supervisor, Root)
23 + end)
24 +
25 + defp walk(pid, parent, type, id) do
26 + kind =
27 + cond do
28 + type == :supervisor -> "sup"
29 + match?({:w, _}, id) -> "worker"
30 + true -> "sys"
31 + end
32 +
33 + kids =
34 + if type == :supervisor,
35 + do:
36 + (try do
37 + Supervisor.which_children(pid)
38 + catch
39 + :exit, _ -> []
40 + end),
41 + else: []
42 +
43 + [
44 + %{pid: pid, parent: parent, kind: kind, label: inspect(id)}
45 + | Enum.flat_map(kids, fn {cid, p, t, _} ->
46 + if is_pid(p), do: walk(p, pid, t, cid), else: []
47 + end)
48 + ]
49 + end
50 +
51 + def graph do
52 + ns = tree()
53 + ids = MapSet.new(ns, & &1.pid)
54 + sup = MapSet.new(for n <- ns, n.parent, do: {n.parent, n.pid})
55 + e = fn a, b, t -> %{source: inspect(a), target: inspect(b), type: t} end
56 + sups = for n <- ns, n.parent, do: e.(n.parent, n.pid, "sup")
57 +
58 + links =
59 + for n <- ns,
60 + {:links, ls} <- [Process.info(n.pid, :links)],
61 + l <- ls,
62 + l in ids,
63 + n.pid < l,
64 + {n.pid, l} not in sup,
65 + {l, n.pid} not in sup,
66 + do: e.(n.pid, l, "link")
67 +
68 + mons =
69 + for n <- ns,
70 + {:monitors, ms} <- [Process.info(n.pid, :monitors)],
71 + {:process, m} <- ms,
72 + m in ids,
73 + do: e.(n.pid, m, "mon")
74 +
75 + %{
76 + nodes:
77 + for(
78 + n <- ns,
79 + do: %{id: inspect(n.pid), label: "#{n.label} #{inspect(n.pid)}", kind: n.kind}
80 + ),
81 + links: sups ++ links ++ mons
82 + }
83 + end
84 + end
85 +
86 + # randomly monitors, links, unlinks, demonitors peers — and sometimes crashes
87 + defmodule Worker do
88 + use GenServer
89 + def start_link(_), do: GenServer.start_link(__MODULE__, [])
90 +
91 + def init(m),
92 + do:
93 + (
94 + Process.flag(:trap_exit, true)
95 + tick()
96 + {:ok, m}
97 + )
98 +
99 + defp tick, do: Process.send_after(self(), :tick, 500 + :rand.uniform(2000))
100 +
101 + def handle_info(:tick, mons) do
102 + tick()
103 + peers = for %{kind: "worker", pid: p} <- Sup.tree(), p != self(), do: p
104 + {:links, links} = Process.info(self(), :links)
105 + links = links -- Process.get(:"$ancestors")
106 +
107 + case :rand.uniform(20) do
108 + n when n <= 6 and peers != [] ->
109 + {:noreply, [Process.monitor(Enum.random(peers)) | mons]}
110 +
111 + n when n <= 10 and mons != [] ->
112 + [r | rest] = Enum.shuffle(mons)
113 + Process.demonitor(r, [:flush])
114 + {:noreply, rest}
115 +
116 + n when n <= 14 and peers != [] ->
117 + Process.link(Enum.random(peers))
118 + {:noreply, mons}
119 +
120 + n when n <= 18 and links != [] ->
121 + Process.unlink(Enum.random(links))
122 + {:noreply, mons}
123 +
124 + 20 ->
125 + {:stop, :boom, mons}
126 +
127 + _ ->
128 + {:noreply, mons}
129 + end
130 + end
131 +
132 + def handle_info({:DOWN, r, _, _, _}, mons), do: {:noreply, List.delete(mons, r)}
133 + def handle_info(_, s), do: {:noreply, s}
134 + end
135 +
136 + # raw websocket: push the graph whenever it changes
137 + defmodule Supviz.Socket do
138 + @behaviour Phoenix.Socket.Transport
139 + def child_spec(_),
140 + do: %{id: __MODULE__, start: {Task, :start_link, [fn -> :ok end]}, restart: :transient}
141 +
142 + def connect(_), do: {:ok, nil}
143 +
144 + def init(_),
145 + do:
146 + (
147 + send(self(), :tick)
148 + {:ok, nil}
149 + )
150 +
151 + def handle_in(_, last), do: {:ok, last}
152 +
153 + def handle_info(:tick, last) do
154 + Process.send_after(self(), :tick, 250)
155 + json = Jason.encode!(Sup.graph())
156 + if json == last, do: {:ok, last}, else: {:push, {:text, json}, json}
157 + end
158 +
159 + def terminate(_, _), do: :ok
160 + end
161 +
162 + defmodule Supviz.Endpoint do
163 + use Phoenix.Endpoint, otp_app: :supviz
164 + socket("/ws", Supviz.Socket, websocket: true, longpoll: false)
165 + plug(:index)
166 +
167 + @html ~S"""
168 + <!doctype html><html><head><meta charset=utf-8><title>Supervision tree</title>
169 + <style>body{margin:0;background:#0b0d12;font:12px
170 + monospace;color:#aaa}#k{position:fixed;top:10px;left:10px;z-index:9}</style>
171 + <script src="https://unpkg.com/3d-force-graph"></script></head><body>
172 + <div id=k><b style=color:#f5a623>&#9679; supervisor</b>
173 + <b style=color:#7b8cff>&#9679; worker</b>
174 + <b style=color:#4fd1a5>&#9679; system</b>
175 + &nbsp;
176 + <b style=color:#888>&mdash; supervises</b>
177 + <b style=color:#ff4d6d>&mdash; link</b>
178 + <b style=color:#36c5f0>&rarr; monitor</b></div><div id=g></div>
179 + <script>
180 + const C={sup:'#f5a623',worker:'#7b8cff',sys:'#4fd1a5'},
181 + L={sup:'#888',link:'#ff4d6d',mon:'#36c5f0'};
182 + const G=ForceGraph3D()(document.getElementById('g'))
183 + .backgroundColor('#0b0d12')
184 + .nodeLabel('label').nodeColor(n=>C[n.kind]).nodeVal(n=>n.kind=='sup'?8:2)
185 + .linkColor(l=>L[l.type]).linkWidth(l=>l.type=='sup'?0.6:1.2)
186 + .linkCurvature(l=>l.type=='sup'?0:0.3)
187 + .linkDirectionalParticles(l=>l.type=='mon'?3:0)
188 + .linkDirectionalParticleSpeed(0.01)
189 + .linkDirectionalArrowLength(l=>l.type=='mon'?4:0)
190 + .linkDirectionalArrowRelPos(1);
191 + G.d3Force('link').distance(l=>l.type=='sup'?25:60)
192 + .strength(l=>l.type=='sup'?1:0.03);
193 + const seen=new Map();
194 + (function connect(){ const ws=new WebSocket(`ws://${location.host}/ws/websocket`);
195 + ws.onmessage=e=>{const d=JSON.parse(e.data);
196 + const ns=d.nodes.map(n=>Object.assign(seen.get(n.id)||n,n));
197 + seen.clear();
198 + ns.forEach(n=>seen.set(n.id,n));
199 + G.graphData({nodes:ns,links:d.links});};
200 + ws.onclose=()=>setTimeout(connect,1000); })();
201 + </script></body></html>
202 + """
203 + def index(conn, _), do: conn |> put_resp_content_type("text/html") |> send_resp(200, @html)
204 + end
205 +
206 + sup = fn id, kids, strat ->
207 + %{
208 + id: id,
209 + type: :supervisor,
210 + start:
211 + {Supervisor, :start_link, [kids, [strategy: strat, max_restarts: 1000, max_seconds: 1]]}
212 + }
213 + end
214 +
215 + w = fn n -> Supervisor.child_spec(Worker, id: {:w, n}) end
216 +
217 + # a failed child start sends us an exit signal; trap it so we can report it instead of dying
218 + Process.flag(:trap_exit, true)
219 +
220 + case Supervisor.start_link(
221 + [
222 + Supviz.Endpoint,
223 + sup.(:pool_a, Enum.map(1..4, w), :one_for_one),
224 + sup.(:pool_b, [w.(5), w.(6), sup.(:pool_c, Enum.map(7..9, w), :one_for_all)], :rest_for_one)
225 + ],
226 + strategy: :one_for_one,
227 + name: Root,
228 + max_restarts: 1000,
229 + max_seconds: 1
230 + ) do
231 + {:ok, root} ->
232 + # the process evaluating this file exits before the shell starts; don't take Root down with it
233 + Process.unlink(root)
234 + IO.puts("Supervision tree live at http://localhost:#{port}")
235 +
236 + {:error, reason} ->
237 + IO.puts(:stderr, "could not start on port #{port} (already in use?): #{inspect(reason)}")
238 + System.halt(1)
239 + end