Skip to main content

lib/mix/tasks/ttycast.bench.ex

defmodule Mix.Tasks.Ttycast.Bench do
  @moduledoc """
  Run a small local benchmark for recording size, open latency, and snapshot latency.

      mix ttycast.bench --events 1000
  """

  use Mix.Task

  @shortdoc "Benchmark TTYCast recording and seek performance"

  @impl true
  def run(argv) do
    {opts, _args, invalid} = OptionParser.parse(argv, strict: [events: :integer])
    invalid == [] || Mix.raise("usage: mix ttycast.bench [--events N]")

    events = Keyword.get(opts, :events, 1_000)
    dir = Path.join(System.tmp_dir!(), "ttycast-bench-#{System.unique_integer([:positive])}")
    File.mkdir_p!(dir)

    ttycast_path = Path.join(dir, "bench.ttycast")
    asciinema_path = Path.join(dir, "bench.cast")
    asciinema_gz_path = asciinema_path <> ".gz"

    {record_us, :ok} = :timer.tc(fn -> write_ttycast(ttycast_path, events) end)
    {open_us, cast} = :timer.tc(fn -> TTYCast.open!(ttycast_path) end)
    midpoint_ms = div(cast.index.duration_us, 2_000)

    {mid_seek_us, _mid_snapshot} =
      :timer.tc(fn -> TTYCast.snapshot!(cast, time_ms: midpoint_ms, format: :plain) end)

    {snapshot_us, snapshot} = :timer.tc(fn -> TTYCast.snapshot!(cast, format: :plain) end)
    :ok = TTYCast.export(cast, :asciinema, asciinema_path)
    File.write!(asciinema_gz_path, :zlib.gzip(File.read!(asciinema_path)))

    report = %{
      events: events,
      terminal_contains_last?: String.contains?(snapshot, "line #{events}"),
      ttycast_bytes: file_size(ttycast_path),
      asciinema_bytes: file_size(asciinema_path),
      asciinema_gzip_bytes: file_size(asciinema_gz_path),
      record_ms: div(record_us, 1_000),
      open_us: open_us,
      mid_seek_us: mid_seek_us,
      snapshot_us: snapshot_us,
      path: ttycast_path
    }

    Mix.shell().info(inspect(report, pretty: true))
  end

  defp write_ttycast(path, events) do
    {:ok, writer} = TTYCast.start_writer(path: path, width: 80, height: 24, chunk_bytes: 64_000)

    Enum.each(1..events, fn index ->
      TTYCast.Writer.write(writer, "line #{index}\r\n")
    end)

    TTYCast.Writer.close(writer)
  end

  defp file_size(path), do: path |> File.stat!() |> Map.fetch!(:size)
end