# Runtime introspection with VegaLite

## Introduction

In this notebook, we will use `Kino` and `VegaLite`
to introspect and plot how our system behaves over
time. If you are not familiar with VegaLite, [read
its introductory notebook](/explore/notebooks/intro-to-vega-lite).

## Setup

Let's add `:vega_lite` and `:kino` as dependencies:

```elixir
Mix.install([
  {:vega_lite, "~> 0.1.2"},
  {:kino, "~> 0.5.0"}
])
```

Let's also define a convenience shortcut for the
VegaLite module:

```elixir
alias VegaLite, as: Vl
```

## Connecting to a remote node

Our goal is to introspect an Elixir node. The code we will
write in this notebook can be used to introspect any running
Elixir node. It can be a development environment that you would
start with:

```
iex --name my_app@IP -S mix TASK
```

Or a production node assembled via
[`mix release`](https://hexdocs.pm/mix/Mix.Tasks.Release.html).

In order to connect two nodes, we need to know their node name
and their cookie. We can get this information for the Livebook
runtime like this:

```elixir
IO.puts node()
IO.puts Node.get_cookie()
```

We will capture this information using Kino inputs. However,
for convenience, we will use the node and cookie of the current
notebook as default values. This means that, if you don't have
a separate Elixir, the runtime will connect and introspect itself.
Let's render the inputs:

```elixir
node_input = Kino.Input.text("Node", default: node())
cookie_input = Kino.Input.text("Cookie", default: Node.get_cookie())

Kino.render(node_input)
Kino.render(cookie_input)
:ok
```

Now let's read the inputs, configure the cookie, and connect to the
other node:

```elixir
node =
  node_input
  |> Kino.Input.read()
  |> String.to_atom()

cookie =
  cookie_input
  |> Kino.Input.read()
  |> String.to_atom()

Node.set_cookie(node, cookie)
true = Node.connect(node)
```

Having successfully connected, let's try spawning a process
on the remote node!

```elixir
Node.spawn(node, fn ->
  IO.inspect(node())
end)
```

## Inspecting processes

Now we are going to extract some information from the running node on our own!

Let's get the list of all processes in the system:

```elixir
remote_pids = :rpc.call(node, Process, :list, [])
```

Wait, but what is this `:rpc.call/4` thing? 🤔

Previously we used `Node.spawn/2` to run a process on the other node
and we used the `IO` module to get some output. However, now
we actually care about the resulting value of `Process.list/0`!

We could still use `Node.spawn/2` to send us the results, which
we would `receive`, but doing that over and over can be quite tedious.
Fortunately, `:rpc.call/4` does essentially that - evaluates the given
function on the remote node and returns its result.

Now, let's gather more information about each process 🕵️

```elixir
processes =
  Enum.map(remote_pids, fn pid ->
    # Extract interesting process information
    info = :rpc.call(node, Process, :info, [pid, [:reductions, :memory, :status]])
    # The result of inspect(pid) is relative to the node
    # where it was called, that's why we call it on the remote node
    pid_inspect = :rpc.call(node, Kernel, :inspect, [pid])

    %{
      pid: pid_inspect,
      reductions: info[:reductions],
      memory: info[:memory],
      status: info[:status]
    }
  end)
```

Having all that data, we can now visualize it on a scatter plot
using VegaLite:

```elixir
Vl.new(width: 600, height: 400)
|> Vl.data_from_values(processes)
|> Vl.mark(:point, tooltip: true)
|> Vl.encode_field(:x, "reductions", type: :quantitative, scale: [type: "log", base: 10])
|> Vl.encode_field(:y, "memory", type: :quantitative, scale: [type: "log", base: 10])
|> Vl.encode_field(:color, "status", type: :nominal)
|> Vl.encode_field(:tooltip, "pid", type: :nominal)
```

From the plot we can easily see which processes do the most work
and take the most memory.

## Tracking memory usage

So far we have used VegaLite to draw static plots. However, we can
Kino to dynamically push data to VegaLite. Let's use them together
to plot the runtime memory usage over time.

There's a very simple way to determine current memory usage in the VM:

```elixir
:erlang.memory()
```

Now let's build a dynamic VegaLite graph. Instead of returning the
VegaLite specification as is, we will wrap it in `Kino.VegaLite.new/1`
to make it dynamic:

```elixir
memory_plot =
  Vl.new(width: 600, height: 400, padding: 20)
  |> Vl.repeat(
    [layer: ["total", "processes", "atom", "binary", "code", "ets"]],
    Vl.new()
    |> Vl.mark(:line)
    |> Vl.encode_field(:x, "iter", type: :quantitative, title: "Measurement")
    |> Vl.encode_repeat(:y, :layer, type: :quantitative, title: "Memory usage (MB)")
    |> Vl.encode(:color, datum: [repeat: :layer], type: :nominal)
  )
  |> Kino.VegaLite.new()
```

Now we can use `Kino.VegaLite.periodically/4` to create a self-updating
plot of memory usage over time on the remote node:

```elixir
Kino.VegaLite.periodically(memory_plot, 200, 1, fn i ->
  point =
    :rpc.call(node, :erlang, :memory, [])
    |> Enum.map(fn {type, bytes} -> {type, bytes / 1_000_000} end)
    |> Map.new()
    |> Map.put(:iter, i)

  Kino.VegaLite.push(memory_plot, point, window: 1000)
  {:cont, i + 1}
end)
```

Unless you connected to a production node, the memory usage
most likely doesn't change, so to emulate some spikes you can
run the following code:

**Binary usage**

```elixir
for i <- 1..10_000 do
  String.duplicate("cat", i)
end
```

**ETS usage**

```elixir
tid = :ets.new(:users, [:set, :public])

for i <- 1..1_000_000 do
  :ets.insert(tid, {i, "User #{i}"})
end
```

In the next notebook, we will learn [how to use `Kino.Control`
to build a chat app](/explore/notebooks/chat-app)!
