| 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:4000/") |
| 235 | |
| 236 | {:error, reason} -> |
| 237 | IO.puts(:stderr, "could not start on port 4000 (already in use?): #{inspect(reason)}") |
| 238 | System.halt(1) |
| 239 | end |
| 240 | |
| 241 |
dch / Sup - visualise OTP supervision trees
Last active 4 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.