Powered by AppSignal & Oban Pro

Sim

livebooks/sim.livemd

Sim

Mix.install([
  {:kino, "~> 0.10.0", only: [:dev]},
  {:ximula, path: Path.join(__DIR__, ".."), env: :dev}
])

Structs

defmodule Sim.Entity do
  # %Sim.Vegetation{size: 5, capacity: 50, ...}
  defstruct id: nil, type: Sim.Entity, value: 0
end

defmodule Sim.Field do
  defstruct position: nil,
            priority: :normal,
            vegetation: [],
            herbivores: [],
            predators: [],
            factories: [],
            buildings: [],
            transports: [],
            pawns: []
end

Data

defmodule Sim.Data do
  alias Ximula.{Grid, Torus}
  alias Ximula.Gatekeeper.Agent, as: Gatekeeper
  alias Sim.Field

  def create(size) do
    Torus.create(size, size, fn x, y ->
      %Field{
        position: {x, y},
        vegetation: %Sim.Entity{type: Sim.Vegetation}
      }
    end)
  end

  def get_field(pid, {x, y}) do
    Gatekeeper.get(pid, fn map -> Torus.get(map, x, y) end)
  end

  def get_all_fields(pid) do
    Gatekeeper.get(pid, &Grid.values(&1))
  end

  def positions(pid) do
    Gatekeeper.get(pid, &Grid.map(&1, fn x, y, _ -> {x, y} end))
  end

  def lock_field(pid, position) do
    Gatekeeper.lock(pid, position, &Grid.get(&1, position))
  end

  def update_field(pid, position, field) do
    Gatekeeper.update(pid, position, field, &Grid.put(&1, position, field))
  end
end
{:ok, agent} = Agent.start_link(fn -> Sim.Data.create(20) end)
{:ok, gatekeeper} = Ximula.Gatekeeper.Server.start_link(context: %{agent: agent})

Sim.Data.get_field(gatekeeper, {0, 0})

Simulations

defmodule Sim.Vegetation do
  alias Sim.Data

  def change(position, gatekeeper) do
    field = gatekeeper |> Data.lock_field(position) |> sim()
    :ok = Data.update_field(gatekeeper, position, field)
    position
  end

  def sim(field) do
    if field.position == {9, 9}, do: raise("Sim Error")
    update_in(field.vegetation.value, &(&1 + 1))
  end
end

defmodule Sim.Herbivore do
  def change(position, _world) do
    position
  end
end

defmodule Sim.Predator do
  def change(position, _world) do
    Process.sleep(10)
    position
  end
end

defmodule Sim.Factory do
  def change(position, _world) do
    Process.sleep(20)
    position
  end
end

defmodule Sim.Transport do
  def change(position, _world) do
    Process.sleep(50)
    position
  end
end

Simulator

defmodule FieldSimulator do
  alias Ximula.Simulator
  alias Ximula.Sim.Queue
  alias Sim.Data

  @simulations [
    Sim.Vegetation,
    Sim.Herbivore,
    Sim.Predator,
    Sim.Factory,
    Sim.Transport
  ]

  def run_queue(%Queue{} = queue, opts) do
    Enum.map(@simulations, &sim_simulation(queue, &1, opts))
    |> aggregate_results(queue.name)
    |> notify_sum()
  end

  def sim_simulation(queue, simulation, opts) do
    Simulator.benchmark(fn ->
      get_positions(opts[:proxy], queue.name)
      |> Simulator.sim({simulation, :change, [opts[:proxy]]})
      |> handle_success(opts[:proxy])
      |> handle_failed(opts[:proxy])
      |> summarize(simulation)
      |> notify()
    end)
  end

  def get_positions(proxy, _name) do
    Data.positions(proxy)
  end

  def handle_success(%{ok: fields} = results, _proxy) do
    IO.puts("successful simulations: #{Enum.count(fields)}")
    results
  end

  def handle_failed(%{exit: failed} = results, _proxy) do
    Enum.each(failed, fn reason ->
      IO.puts("failed simulations: #{Exception.format_exit(reason)}")
    end)

    results
  end

  def summarize(%{ok: success, exit: failed}, simulation) do
    %{
      simulation: simulation,
      ok: success,
      error:
        Enum.map(failed, fn {id, {exception, stacktrace}} ->
          {id, Exception.normalize(:exit, exception, stacktrace) |> Exception.message()}
        end)
    }
  end

  # [{1097, %{error: [], ok: [], simulation: Sim.Vegetation}}]
  def aggregate_results(results, queue) do
    %{
      queue: queue,
      results:
        Enum.map(results, fn {time, %{error: error, ok: ok, simulation: simulation}} ->
          %{simulation: simulation, time: time, errors: Enum.count(error), ok: Enum.count(ok)}
        end)
    }
  end

  def notify_sum(results) do
    # PubSub.broadcast(topic, queue_result) | GenStage.cast(stage, {:receive, queue_result})
    dbg(results)
  end

  def notify(%{error: _error, ok: _ok, simulation: _simulation} = result) do
    # %{simulation: simulation, changed: ok} |> dbg()
    # %{simulation: simulation, failed: error} |> dbg()
    # PubSub.broadcast(topic, change) | GenStage.cast(stage, {:receive, change})
    result
  end
end
Task.Supervisor.start_link(name: Ximula.Simulator.Task.Supervisor)
Task.Supervisor.start_link(name: Ximula.Sim.Loop.Task.Supervisor)
Ximula.Sim.Loop.start_link(sim_args: [proxy: gatekeeper])
Ximula.Sim.Loop.add_queue(%Ximula.Sim.Queue{
  name: :high,
  func: {FieldSimulator, :run_queue, [proxy: gatekeeper]},
  interval: 10_000
})

# Ximula.Sim.Loop.add_queue(%Ximula.Sim.Queue{
#  name: :normal,
#  func: &FieldSimulator.run_queue/2,
#  interval: 2_000
# })

# Ximula.Sim.Loop.add_queue(%Ximula.Sim.Queue{
#  name: :low,
#  func: &FieldSimulator.run_queue/2,
#  interval: 10_000
# })
require Logger
Logger.info("START!")
Ximula.Sim.Loop.start_sim()
Process.sleep(22_000)
Ximula.Sim.Loop.stop_sim()
Logger.info("END!")

Notes

Grid size 10x10

Whole queue took about 700ms

vegetation does +1 -> 5 -10 ms

herbivore does nothing -> 3- 5 ms

predator sleep 10ms -> ~88ms

factory sleep 20ms -> ~168ms

transport sleep 50ms -> ~410ms

all overhead about 30 - 100 μs per thread

Sim.Data.get_field(gatekeeper, {0, 0}) |> dbg()
Sim.Data.get_field(gatekeeper, {9, 9}) |> dbg()