dch

dch / Sup - visualise OTP supervision trees

Last active 6 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.

Revision 6003ee55f740167ce0ad09b0930abf6139987bb2

sup Raw
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
5Mix.install([{:phoenix, "~> 1.8"}, {:bandit, "~> 1.5"}, {:jason, "~> 1.4"}])
6# ignore the intentional crash reports
7Logger.configure(level: :none)
8
9Application.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
17defmodule 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
84end
85
86# randomly monitors, links, unlinks, demonitors peers — and sometimes crashes
87defmodule 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}
134end
135
136# raw websocket: push the graph whenever it changes
137defmodule 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
160end
161
162defmodule 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)
204end
205
206sup = 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 }
213end
214
215w = 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
218Process.flag(:trap_exit, true)
219
220case 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:4000/")
235
236 {:error, reason} ->
237 IO.puts(:stderr, "could not start on port 4000 (already in use?): #{inspect(reason)}")
238 System.halt(1)
239end
240
241