| @@ -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 / Sup - visualise OTP supervision trees
Last active 5 hours ago
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.
| @@ -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>● supervisor</b> | |
| 173 | + | <b style=color:#7b8cff>● worker</b> | |
| 174 | + | <b style=color:#4fd1a5>● system</b> | |
| 175 | + | | |
| 176 | + | <b style=color:#888>— supervises</b> | |
| 177 | + | <b style=color:#ff4d6d>— link</b> | |
| 178 | + | <b style=color:#36c5f0>→ 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 | |