Packages

Multi-surface application runtime for Elixir. One TEA module renders to terminal, browser (LiveView), SSH, and MCP (agents). 30+ widgets, flexbox + CSS grid, AI agent runtime, distributed swarm with CRDTs, time-travel debugging, session recording, sandboxed REPL, and agentic commerce.

Current section

Files

Jump to
raxol lib raxol ui rendering pipeline scheduler.ex
Raw

lib/raxol/ui/rendering/pipeline/scheduler.ex

defmodule Raxol.UI.Rendering.Pipeline.Scheduler do
@moduledoc """
Render scheduling and debouncing for the pipeline.
Manages when renders are executed to optimize performance.
"""
require Logger
alias Raxol.UI.Rendering.Pipeline.Stages
@type state :: map()
@type diff_result :: term()
@type tree :: map() | nil
# Render debounce delay in milliseconds
@render_debounce_ms if Mix.env() == :test, do: 5, else: 10
@doc """
Schedules or immediately executes a render based on current state.
Implements debouncing to batch rapid updates.
"""
@spec schedule_or_execute_render(diff_result(), tree(), state()) :: state()
def schedule_or_execute_render(
_diff_result,
_tree,
%{render_scheduled_for_next_frame: true} = state
) do
Logger.debug(
"Pipeline: Render already scheduled for next frame, skipping additional scheduling."
)
state
end
def schedule_or_execute_render(
_diff_result,
_tree,
%{render_timer_ref: timer_ref} = state
)
when not is_nil(timer_ref) do
Logger.debug("Pipeline: Cancelling existing render timer and rescheduling.")
Process.cancel_timer(timer_ref)
schedule_render(state)
end
def schedule_or_execute_render(_diff_result, _tree, state) do
schedule_render(state)
end
@doc """
Schedules a render after the debounce delay.
"""
@spec schedule_render(state()) :: state()
def schedule_render(state) do
timer_ref = Process.send_after(self(), :execute_render, @render_debounce_ms)
%{state | render_timer_ref: timer_ref}
end
@doc """
Executes the actual render pipeline.
"""
@spec execute_render(state()) :: state()
def execute_render(state) do
execute_render_with_tree(state.current_tree, state)
end
@doc """
Marks that a render should occur on the next animation frame.
"""
@spec mark_render_for_next_frame(state()) :: state()
def mark_render_for_next_frame(state) do
%{state | render_scheduled_for_next_frame: true}
end
# Helper functions for if-statement elimination
defp execute_render_with_tree(nil, state) do
Logger.debug(
"Pipeline: Execute render called but no current tree available."
)
%{
state
| render_timer_ref: nil,
render_scheduled_for_next_frame: false
}
end
defp execute_render_with_tree(current_tree, state) do
Logger.debug("Pipeline: Executing render with current tree.")
start_time = System.monotonic_time(:microsecond)
# Execute the rendering pipeline stages
{painted_output, composed_tree} =
Stages.execute_render_stages(
{:replace, current_tree},
current_tree,
state.renderer_module,
state.previous_composed_tree,
state.previous_painted_output
)
# Commit the painted output to the renderer
commit_painted_output(painted_output, state.renderer_module)
end_time = System.monotonic_time(:microsecond)
render_time_ms = (end_time - start_time) / 1000
Logger.debug("Pipeline: Render completed in #{render_time_ms}ms")
%{
state
| render_timer_ref: nil,
render_scheduled_for_next_frame: false,
last_render_time: render_time_ms,
previous_composed_tree: composed_tree,
previous_painted_output: painted_output
}
end
defp commit_painted_output(nil, _renderer_module), do: :ok
defp commit_painted_output(painted_output, renderer_module) do
commit_to_renderer(painted_output, renderer_module)
end
@spec commit_to_renderer(term(), module()) :: :ok
defp commit_to_renderer(painted_output, renderer_module) do
renderer = renderer_module || Raxol.UI.Rendering.Renderer
case Raxol.Core.ErrorHandling.safe_call(fn ->
renderer.render(painted_output)
end) do
{:ok, _} ->
Logger.debug(
"Pipeline: Committed output to renderer #{inspect(renderer)}"
)
{:error, error} ->
Logger.error(
"Pipeline: Failed to commit to renderer: #{inspect(error)}"
)
end
:ok
end
@doc """
Gets the render debounce delay in milliseconds.
"""
@spec debounce_delay() :: non_neg_integer()
def debounce_delay(), do: @render_debounce_ms
end