ActorSimulation (GenServerVirtualTime v0.6.0)

Copy Markdown View Source

A DSL for simulating actor systems with message rates and statistics.

This module provides a way to define actors, their message sending patterns, and simulate their interactions using virtual time.

Example

simulation =
  ActorSimulation.new()
  |> ActorSimulation.add_actor(:producer,
      send_pattern: {:periodic, 100, {:data, :id}},
      targets: [:consumer])
  |> ActorSimulation.add_actor(:consumer,
      on_receive: fn msg, state ->
        # Process message and maybe send response
        {:ok, state}
      end)
  |> ActorSimulation.run(duration: 5000)

stats = ActorSimulation.get_stats(simulation)
IO.inspect(stats)

Summary

Functions

Adds an actor to the simulation.

Adds a real GenServerVirtualTime process to the simulation ("Process in the Loop").

Helper to collect current stats during simulation (for termination conditions). Can be called from terminate_when functions.

Enables message tracing for the simulation.

Generates a Mermaid flowchart report showing actor topology and statistics.

Gets statistics from the simulation.

Gets the message trace from the simulation. Returns a list of trace events for building sequence diagrams.

Creates a new actor simulation.

Runs the simulation for the specified duration (in milliseconds).

Stops the simulation and cleans up resources.

Formats the trace as a Mermaid sequence diagram with enhanced styling.

Writes a flowchart report directly to a file.

Functions

add_actor(simulation, name, opts \\ [])

Adds an actor to the simulation.

Options:

  • :send_pattern - How this actor sends messages:
    • {:periodic, interval, message} - Send message every interval ms
    • {:rate, messages_per_second, message} - Send at a specific rate
    • {:burst, count, interval, message} - Send count messages every interval
  • :targets - List of actor names to send messages to
  • :on_receive - Function called when receiving a message: fn msg, state -> {:ok, new_state} | {:send, msgs, new_state} end

  • :on_match - Pattern matching responses: [{pattern, response_fn}]
  • :initial_state - Initial state for the actor (default: %{})

add_process(simulation, name, opts \\ [])

Adds a real GenServerVirtualTime process to the simulation ("Process in the Loop").

This allows you to test real GenServer implementations alongside simulated actors.

Options:

  • :module - The GenServer module to start (required)
  • :args - Arguments to pass to the module's init/1
  • :targets - List of actor names this process can send to (optional)

Example

defmodule MyRealServer do
  use VirtualTimeGenServer

  def init(args), do: {:ok, args}
  def handle_call(:ping, _from, state), do: {:reply, :pong, state}
end

simulation =
  ActorSimulation.new()
  |> ActorSimulation.add_process(:my_server, module: MyRealServer, args: %{})
  |> ActorSimulation.add_actor(:pinger,
      send_pattern: {:periodic, 100, {:call, :my_server, :ping}})

collect_current_stats(simulation)

Helper to collect current stats during simulation (for termination conditions). Can be called from terminate_when functions.

Note: This is a live snapshot and may be called multiple times during run/2.

enable_trace(simulation)

Enables message tracing for the simulation.

generate_flowchart_report(simulation, opts \\ [])

Generates a Mermaid flowchart report showing actor topology and statistics.

This creates a visual report with:

  • Actor topology as a flowchart
  • Statistics embedded in nodes (message counts, rates)
  • Activity-based styling (color coding by traffic)
  • Complete HTML page with stats table

Options

  • :title - Report title (default: "Simulation Report")
  • :show_stats_on_nodes - Show stats on nodes (default: true)
  • :show_message_labels - Show message types on edges (default: true)
  • :layout - Direction: "TB", "LR", "RL", "BT" (default: "TB")
  • :style_by_activity - Color by activity level (default: true)

Example

simulation = ActorSimulation.new()
  |> add_actor(:producer, send_pattern: {:periodic, 100, :data}, targets: [:consumer])
  |> add_actor(:consumer)
  |> run(duration: 1000)

html = ActorSimulation.generate_flowchart_report(simulation,
  title: "My System",
  layout: "LR")

File.write!("report.html", html)

Based on Mermaid Flowchart Syntax

get_stats(simulation)

Gets statistics from the simulation.

get_trace(simulation)

Gets the message trace from the simulation. Returns a list of trace events for building sequence diagrams.

Each event is a map with:

  • :timestamp - Virtual time when message was sent
  • :from - Sender actor name
  • :to - Receiver actor name
  • :message - The message sent
  • :type - :cast, :call, or :send

new(opts \\ [])

Creates a new actor simulation.

Options:

  • :trace - Enable message tracing for sequence diagrams (default: false)

Example

iex> simulation = ActorSimulation.new()
iex> is_pid(simulation.clock)
true
iex> simulation.actors
%{}

run(simulation, opts \\ [])

Runs the simulation for the specified duration (in milliseconds).

Options:

  • :duration - Maximum duration in milliseconds (default: 10,000)
  • :terminate_when - Function that takes simulation and returns true to stop
  • :check_interval - How often to check termination condition in ms (default: 100)
  • :expected_messages - Integer count of expected messages to receive before terminating

Boundary Condition Handling

The simulation automatically handles boundary conditions where events are scheduled at exactly the end time. For example, a rate pattern sending 50 messages per second over 1000ms will send messages at times 0, 20, 40, ..., 980, 1000. The simulation advances the clock one tick beyond the specified duration and waits for quiescence to ensure all boundary events are processed.

Example

# Run for fixed duration (backward compatible)
simulation = ActorSimulation.run(simulation, duration: 5000)

# Run until condition met (new feature)
simulation = ActorSimulation.run(simulation,
  max_duration: 10_000,
  terminate_when: fn sim ->
    # Stop when all actors have sent at least 10 messages
    stats = ActorSimulation.collect_current_stats(sim)
    Enum.all?(stats.actors, fn {_name, s} -> s.sent_count >= 10 end)
  end
)

# Run until expected number of messages (convenience)
simulation = ActorSimulation.run(simulation,
  max_duration: 10_000,
  expected_messages: 1000
)

stop(simulation)

Stops the simulation and cleans up resources.

trace_to_mermaid(simulation, opts \\ [])

Formats the trace as a Mermaid sequence diagram with enhanced styling.

Mermaid is widely supported in GitHub, GitLab, and many markdown viewers. Uses features from Mermaid sequence diagrams:

  • Different arrow types for call/cast/send
  • Activation boxes for processing
  • Notes with timestamps
  • Background highlighting for grouped interactions
  • Termination indicators showing when simulation stopped

Example

iex> simulation = %ActorSimulation{trace: [
...>   %{from: :alice, to: :bob, message: :hello, type: :send, timestamp: 100},
...>   %{from: :bob, to: :alice, message: :hi, type: :send, timestamp: 200}
...> ]}
iex> mermaid = ActorSimulation.trace_to_mermaid(simulation)
iex> String.contains?(mermaid, "sequenceDiagram")
true
iex> String.contains?(mermaid, "alice->>bob")
true

write_flowchart_report(simulation, filename, opts \\ [])

Writes a flowchart report directly to a file.

Same options as generate_flowchart_report/2.

Example

simulation = ActorSimulation.new()
  |> add_actor(:source, send_pattern: {:periodic, 100, :msg}, targets: [:sink])
  |> add_actor(:sink)
  |> run(duration: 500)

{:ok, path} = ActorSimulation.write_flowchart_report(
  simulation,
  "my_report.html",
  title: "Production System")