# `Cauldron2D.Net.Load`
[🔗](https://github.com/jaman/cauldron/blob/v0.1.3/cauldron_2d_net/lib/cauldron_2d/net/load.ex#L1)

A load test against a running server: players and watchers joining its arenas over
the wire, the frames they get measured against the steps the server runs, and the
server's own numbers (`Cauldron2D.Net.Api`'s `/stats`) alongside.

    Cauldron2D.Net.Load.run(url: "http://arcade:2280/api", actions: ["left", "right", "fire"], players: 40, watchers: 100, seconds: 60)
    Cauldron2D.Net.Load.run(url: "http://arcade:2280/api", actions: [], players: 10, ramp: 10, every: 5, seconds: 120)

Sessions are accounts `load_1`, `load_2`… (password `load test`), registered on the
server if they are not there, and spread over the arenas in turn. A report goes out
every `:every` seconds: a line with the sessions joined, the frames a second they
got (mean and least) against the ticks a second the server's worlds are set to, the
longest gap between frames, and the node (how busy its schedulers were since the
last report, the run queue, processes, memory); then a line for each of the three
arenas with the longest ticks — the mean, the 95th percentile and the longest against
the tick's budget, and the share of the last ticks that ran more than one step
(`behind`; a little is timer jitter, more is a world that cannot keep up).

A run is **degraded** when, over one report, any world's 95th-percentile tick is
longer than its budget (`1 / hz`), any world ran behind on more than a fifth of its
ticks (a few double steps are the timer firing late on a busy node, not a world that
cannot keep up — its ticks say which), the least frame rate any session saw fell
under 80% of the tick rate, or a session waited over a second for a frame. The
report says so and why. With
`:ramp`, that many more sessions join at every report until the run is degraded —
the sessions before the last addition are the `capacity` in the summary — or the
time is up.

`stop/0` from another process ends a run early, after the interval in progress,
with the summary of what was measured; a mix task can call it on a key press.

## Options

  * `:url` — the server's API, default `http://localhost:2280/api`
  * `:actions` — the action names a player holds at random. Required
  * `:topic` — the channel's topic prefix, default `"world"`
  * `:players`, `:watchers` — how many of each to start with, default 10 and 0
  * `:arenas` — the ids of the arenas to use, default every arena the server lists
  * `:seconds` — how long the run lasts, default 30
  * `:every` — seconds between reports (and ramp steps), default 5
  * `:ramp` — sessions to add at every report, default none; they are players and
    watchers in the same proportion as the start
  * `:audio` — `:personal` (default) for a session that takes the server's sound as a
    browser would, `:off` for one that asks for none
  * `:password` — the sessions' password, default `load test`
  * `:report` — a function given each report, default `IO.puts/1`
  * `:out` — a file every report and the summary are appended to as JSON, one
    object a line, as the run goes, so a run cut short still leaves its record.
    Default none

Returns the summary: `sessions`, `joined`, `refused`, `refusals` (reasons with their
counts), `fps` (`mean`, `min` over the whole run), `gap_ms` (`max`), `join_ms`
(`mean`, `max`), `server` (the last `arenas` and `node` stats, `hz` and
`steps_per_second`), `degraded` (`%{sessions, why}` for the first degraded report,
else `nil`), `capacity` and `stopped` (`:time`, `:degraded` or `:asked`).

# `summary`

```elixir
@type summary() :: %{
  sessions: non_neg_integer(),
  joined: non_neg_integer(),
  refused: non_neg_integer(),
  refusals: %{required(term()) =&gt; pos_integer()},
  fps: %{mean: float(), min: float()},
  gap_ms: %{max: non_neg_integer()},
  join_ms: %{mean: non_neg_integer(), max: non_neg_integer()},
  server: %{
    arenas: [map()],
    node: map(),
    hz: number(),
    steps_per_second: float()
  },
  degraded: %{sessions: non_neg_integer(), why: [String.t()]} | nil,
  capacity: non_neg_integer() | nil,
  stopped: :time | :degraded | :asked
}
```

# `degraded`

```elixir
@spec degraded(map()) :: [String.t()]
```

Why an interval counts as degraded, as `run/1` judges it: `[]` when it does not.

# `run`

```elixir
@spec run(keyword()) :: summary()
```

Run a load test; see the module documentation for the options and the summary.

# `stop`

```elixir
@spec stop() :: :ok
```

End the run in progress after the interval it is measuring; nothing when none is running.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
