Raxol.Recording.Index (Raxol v2.7.0)

View Source

Keyframe index over a recorded session, so seeking is not a prefix replay.

Without an index, reconstructing the screen at time t means feeding every output event from the start of the recording into an emulator: O(n) per seek, which makes a drag-scrub over a long recording unusable. This module walks the session once, snapshots the terminal at fixed time intervals, and then answers "what did the screen look like at t?" by starting from the nearest snapshot at or before t and replaying forward only.

The replay path is the one Raxol.Test.CrossTerminal.AnsiReplayer established: Raxol.Terminal.Emulator.new/2 -> process_input/2 -> get_screen_buffer/1.

What a keyframe carries

A keyframe holds both a :buffer (a plain Raxol.Core.Buffer map, which is what Raxol.LiveView.TerminalBridge and Raxol.Core.Renderer render) and the :emulator it came from. The emulator is not redundant: resuming a replay needs the cursor position, pending SGR attributes, scroll region, and charset state, none of which survive in a screen buffer. Rendering wants the buffer; seeking wants the emulator.

index = Index.build(session)
Index.buffer_at(index, session, 42_000_000)
Index.keyframe_before(index, 42_000_000).us

Usage

iex> session = %Raxol.Recording.Session{width: 80, height: 24, events: []}
iex> index = Raxol.Recording.Index.build(session)
iex> {length(index.keyframes), index.marks, index.duration_us}
{1, [], 0}

Summary

Functions

Screen state at an arbitrary time: nearest keyframe, then forward replay only.

Builds a keyframe index for a session.

Nearest keyframe at or before us.

Approximate heap size of an index in bytes.

Captures an emulator's visible screen as a Raxol.Core.Buffer map.

Types

keyframe()

@type keyframe() :: %{
  us: non_neg_integer(),
  event_index: non_neg_integer(),
  buffer: Raxol.Core.Buffer.t(),
  emulator: Raxol.Terminal.Emulator.t()
}

t()

@type t() :: %{
  keyframes: [keyframe()],
  marks: [non_neg_integer()],
  duration_us: non_neg_integer(),
  event_count: non_neg_integer(),
  interval_us: pos_integer(),
  width: pos_integer(),
  height: pos_integer()
}

Functions

buffer_at(index, session, us)

Screen state at an arbitrary time: nearest keyframe, then forward replay only.

Matches Raxol.Recording.Player's seek semantics — every output event with elapsed_us < us is applied, events at exactly us are not — so a seek through the index lands on the same screen as a seek without one. Times past the end of the recording return the final screen.

build(session, opts \\ [])

@spec build(
  Raxol.Recording.Session.t(),
  keyword()
) :: t()

Builds a keyframe index for a session.

Options

  • :interval_us - minimum spacing between keyframes in microseconds (default: 15000000). See the module docs for the trade-off.
  • :width / :height - terminal size (default: the session header's).

Dimensions are clamped. session.width/session.height come verbatim from the .cast header, which is Jason.decode!d without validation, and they are multiplied straight into an Emulator.new/2 allocation -- so mix raxol.replay evil.cast --index on a header claiming 100000x100000 allocated before reading a single event. The keyframe count is capped for the same reason: at the 15s default a 24-hour recording is ~5760 keyframes of emulator snapshots, and --info --index builds one just to print its size.

keyframe_before(map, us)

@spec keyframe_before(t(), non_neg_integer()) :: keyframe()

Nearest keyframe at or before us.

Always succeeds: us: 0 is always a keyframe, so a time before the first event still resolves (to the blank screen the recording started from).

memory_bytes(index)

@spec memory_bytes(t()) :: non_neg_integer()

Approximate heap size of an index in bytes.

Uses :erts_debug.size_shared/1, so terms the keyframes share (cell text, style maps, the buffer nested inside its emulator) are counted once. Reported by mix raxol.replay --info --index so the cost is visible before anyone builds an index for a huge recording.

snapshot(emulator)

Captures an emulator's visible screen as a Raxol.Core.Buffer map.

Exposed because the index is not the only consumer: a live replay surface holds its own emulator and needs the same conversion to render, and tests compare an indexed seek against a full prefix replay through it.