Refatoring
- separated net topology from the simulation nodes - moved files in a better hierarchy
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
package net.berack.upo.valpre;
|
||||
|
||||
import net.berack.upo.valpre.rand.Distribution;
|
||||
import net.berack.upo.valpre.sim.SimulationMultiple;
|
||||
import net.berack.upo.valpre.sim.Net;
|
||||
import net.berack.upo.valpre.sim.ServerNode;
|
||||
|
||||
public class Main {
|
||||
public static void main(String[] args) throws Exception {
|
||||
@@ -12,18 +15,19 @@ public class Main {
|
||||
var sigma = 0.6;
|
||||
|
||||
// Build the network
|
||||
var net = new Net();
|
||||
var node1 = ServerNode.createLimitedSource("Source", new Distribution.Exponential(lambda), total);
|
||||
var node2 = ServerNode.createQueue("Queue", 1, new Distribution.NormalBoxMuller(mu, sigma));
|
||||
node1.addChild(node2, 1.0);
|
||||
|
||||
/// Run the simulation
|
||||
var sim = new NetSimulation();
|
||||
sim.addNode(node1);
|
||||
sim.addNode(node2);
|
||||
net.addNode(node1);
|
||||
net.addNode(node2);
|
||||
net.addConnection(node1.name, node2.name, 1.0);
|
||||
net.normalizeWeights();
|
||||
|
||||
/// Run multiple simulations
|
||||
// var maxDepartures = new EndSimulationCriteria.MaxDepartures("Queue", total);
|
||||
// var maxTime = new EndSimulationCriteria.MaxTime(1000.0);
|
||||
var nano = System.nanoTime();
|
||||
var sim = new SimulationMultiple(net);
|
||||
var results = sim.runParallel(seed, 1000);
|
||||
nano = System.nanoTime() - nano;
|
||||
|
||||
|
||||
@@ -1,310 +0,0 @@
|
||||
package net.berack.upo.valpre;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.PriorityQueue;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import net.berack.upo.valpre.NetStatistics.RunResult;
|
||||
import net.berack.upo.valpre.NetStatistics.Statistics;
|
||||
import net.berack.upo.valpre.rand.Rng;
|
||||
import net.berack.upo.valpre.rand.Rngs;
|
||||
|
||||
/**
|
||||
* A network simulation that uses a discrete event simulation to model the
|
||||
* behavior of a network of servers.
|
||||
*/
|
||||
public class NetSimulation {
|
||||
private final Collection<ServerNode> servers = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Adds a new server node to the network.
|
||||
*
|
||||
* @param node The server node to add.
|
||||
*/
|
||||
public void addNode(ServerNode node) {
|
||||
this.servers.add(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the simualtion multiple times with the given seed and number of runs.
|
||||
* The runs are calculated one after the other. For a parallel run see
|
||||
* {@link #runParallel(long, int, EndCriteria...)}.
|
||||
*
|
||||
* @param seed The seed to use for the random number generator.
|
||||
* @param runs The number of runs to perform.
|
||||
* @param criterias The criteria to determine when to end the simulation. If
|
||||
* null then the simulation will run until there are no more
|
||||
* events.
|
||||
* @return The statistics the network.
|
||||
*/
|
||||
public NetStatistics run(long seed, int runs, EndCriteria... criterias) {
|
||||
var rng = new Rng(seed);
|
||||
var stats = new RunResult[runs];
|
||||
|
||||
for (int i = 0; i < runs; i++) {
|
||||
stats[i] = this.run(rng, criterias);
|
||||
}
|
||||
return new NetStatistics(stats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the simulation multiple times with the given seed and number of runs.
|
||||
* The runs are calculated in parallel using the given number of threads.
|
||||
* The maximum number of threads are determined by the available processors
|
||||
* and the number of runs.
|
||||
*
|
||||
* @param seed The seed to use for the random number generator.
|
||||
* @param runs The number of runs to perform.
|
||||
* @param criterias The criteria to determine when to end the simulation. If
|
||||
* null then the simulation will run until there are no more
|
||||
* events.
|
||||
* @return The statistics the network.
|
||||
* @throws InterruptedException If the threads are interrupted.
|
||||
* @throws ExecutionException If the one of the threads has been aborted.
|
||||
*/
|
||||
public NetStatistics runParallel(long seed, int runs, EndCriteria... criterias)
|
||||
throws InterruptedException, ExecutionException {
|
||||
var rngs = new Rngs(seed);
|
||||
var results = new NetStatistics.RunResult[runs];
|
||||
var futures = new Future[runs];
|
||||
|
||||
var numThreads = Math.min(runs, Runtime.getRuntime().availableProcessors());
|
||||
try (var threads = Executors.newFixedThreadPool(numThreads)) {
|
||||
for (int i = 0; i < runs; i++) {
|
||||
final var id = i;
|
||||
futures[i] = threads.submit(() -> {
|
||||
results[id] = this.run(rngs.getRng(id), criterias);
|
||||
});
|
||||
}
|
||||
|
||||
for (var i = 0; i < runs; i++) {
|
||||
futures[i].get();
|
||||
}
|
||||
|
||||
return new NetStatistics(results);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the simulation until a given criteria is met.
|
||||
*
|
||||
* @param rng The random number generator to use.
|
||||
* @param criterias The criteria to determine when to end the simulation. If
|
||||
* null then the simulation will run until there are no more
|
||||
* events.
|
||||
* @return The statistics the network.
|
||||
*/
|
||||
public NetStatistics.RunResult run(Rng rng, EndCriteria... criterias) {
|
||||
var run = new SimulationRun(this.servers, rng, criterias);
|
||||
while (!run.hasEnded())
|
||||
run.processNextEvent();
|
||||
return run.endSimulation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an entire run of the simulation.
|
||||
*/
|
||||
public static class SimulationRun {
|
||||
private final Map<String, NodeBehavior> nodes;
|
||||
private final PriorityQueue<Event> fel;
|
||||
private final EndCriteria[] criterias;
|
||||
private final long timeStartedNano;
|
||||
private final long seed;
|
||||
private final Rng rng;
|
||||
private double time;
|
||||
|
||||
/**
|
||||
* Creates a new run of the simulation with the given nodes and random number
|
||||
* generator.
|
||||
*
|
||||
* @param nodes The nodes in the network.
|
||||
* @param rng The random number generator to use.
|
||||
* @param criterias when the simulation has to end.
|
||||
*/
|
||||
private SimulationRun(Collection<ServerNode> nodes, Rng rng, EndCriteria... criterias) {
|
||||
this.nodes = new HashMap<>();
|
||||
this.fel = new PriorityQueue<>();
|
||||
this.criterias = criterias;
|
||||
this.timeStartedNano = System.nanoTime();
|
||||
this.seed = rng.getSeed();
|
||||
this.rng = rng;
|
||||
this.time = 0.0d;
|
||||
|
||||
// Initial arrivals (if spawned)
|
||||
for (var node : nodes) {
|
||||
this.nodes.put(node.name, new NodeBehavior());
|
||||
if (node.shouldSpawnArrival(0))
|
||||
this.addArrival(node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the next event in the future event list.
|
||||
* This method will throw NullPointerException if there are no more events.
|
||||
* You should check if the simulation has ended before calling this method.
|
||||
*
|
||||
* @see #hasEnded()
|
||||
*/
|
||||
public void processNextEvent() {
|
||||
var event = fel.poll();
|
||||
var node = this.nodes.get(event.node.name);
|
||||
this.time = event.time;
|
||||
|
||||
switch (event.type) {
|
||||
case ARRIVAL -> {
|
||||
if (node.updateArrival(event.time, event.node.maxServers))
|
||||
this.addDeparture(event.node);
|
||||
}
|
||||
case DEPARTURE -> {
|
||||
if (node.updateDeparture(event.time))
|
||||
this.addDeparture(event.node);
|
||||
|
||||
var next = event.node.getChild(this.rng);
|
||||
if (next != null) {
|
||||
this.addArrival(next);
|
||||
}
|
||||
if (event.node.shouldSpawnArrival(node.stats.numArrivals)) {
|
||||
this.addArrival(event.node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends the simulation and returns the statistics of the network.
|
||||
*
|
||||
* @return The statistics of the network.
|
||||
*/
|
||||
private NetStatistics.RunResult endSimulation() {
|
||||
var elapsed = System.nanoTime() - this.timeStartedNano;
|
||||
var nodes = new HashMap<String, Statistics>();
|
||||
for (var entry : this.nodes.entrySet())
|
||||
nodes.put(entry.getKey(), entry.getValue().stats);
|
||||
|
||||
return new RunResult(this.seed, this.time, elapsed, nodes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current time.
|
||||
*
|
||||
* @return a double representing the current time of the simulation.
|
||||
*/
|
||||
public double getTime() {
|
||||
return this.time;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the node requested by the name passed as a string.
|
||||
*
|
||||
* @param node the name of the node
|
||||
* @return the node
|
||||
*/
|
||||
public NodeBehavior getNode(String node) {
|
||||
return this.getNode(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an arrival event to the future event list. The event is created based
|
||||
* on the given node, and no delay is added.
|
||||
*
|
||||
* @param node The node to create the event for.
|
||||
*/
|
||||
public void addArrival(ServerNode node) {
|
||||
var event = Event.newArrival(node, this.time);
|
||||
fel.add(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a departure event to the future event list. The event is created based
|
||||
* on the given node, and the delay is determined by the node's distribution.
|
||||
*
|
||||
* @param node The node to create the event for.
|
||||
*/
|
||||
public void addDeparture(ServerNode node) {
|
||||
var delay = node.getPositiveSample(this.rng);
|
||||
var event = Event.newDeparture(node, this.time + delay);
|
||||
fel.add(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the simulation has finshed based on the given criteria.
|
||||
*
|
||||
* @return True if the simulation should end, false otherwise.
|
||||
*/
|
||||
public boolean hasEnded() {
|
||||
if (fel.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
for (var c : this.criterias) {
|
||||
if (c.shouldEnd(this)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a summary of the behavior of a server node in the network.
|
||||
* It is used by the simulation to track the number of arrivals and departures,
|
||||
* the maximum queue length, the busy time, and the response time.
|
||||
*/
|
||||
public static class NodeBehavior {
|
||||
public int numServerBusy = 0;
|
||||
public final Statistics stats = new Statistics();
|
||||
private final ArrayDeque<Double> queue = new ArrayDeque<>();
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @param time
|
||||
* @param maxServers
|
||||
* @return
|
||||
*/
|
||||
public boolean updateArrival(double time, int maxServers) {
|
||||
var total = this.stats.averageQueueLength * this.stats.numArrivals;
|
||||
|
||||
this.queue.add(time);
|
||||
this.stats.numArrivals++;
|
||||
this.stats.averageQueueLength = (total + this.queue.size()) / this.stats.numArrivals;
|
||||
this.stats.maxQueueLength = Math.max(this.stats.maxQueueLength, this.queue.size());
|
||||
|
||||
var startDeparture = maxServers > this.numServerBusy;
|
||||
if (startDeparture) {
|
||||
this.numServerBusy++;
|
||||
} else {
|
||||
this.stats.busyTime += time - this.stats.lastEventTime;
|
||||
}
|
||||
|
||||
this.stats.lastEventTime = time;
|
||||
return startDeparture;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @param time
|
||||
* @return
|
||||
*/
|
||||
public boolean updateDeparture(double time) {
|
||||
var startService = this.queue.poll();
|
||||
var response = time - startService;
|
||||
|
||||
var startDeparture = this.queue.size() >= this.numServerBusy;
|
||||
if (!startDeparture) {
|
||||
this.numServerBusy--;
|
||||
}
|
||||
|
||||
this.stats.numDepartures++;
|
||||
this.stats.responseTime += response;
|
||||
this.stats.busyTime += time - this.stats.lastEventTime;
|
||||
this.stats.lastEventTime = time;
|
||||
return startDeparture;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package net.berack.upo.valpre;
|
||||
package net.berack.upo.valpre.sim;
|
||||
|
||||
/**
|
||||
* Criteria to determine when to end the simulation.
|
||||
@@ -10,7 +10,7 @@ public interface EndCriteria {
|
||||
* @param run The current run of the network.
|
||||
* @return True if the simulation should end, false otherwise.
|
||||
*/
|
||||
public boolean shouldEnd(NetSimulation.SimulationRun run);
|
||||
public boolean shouldEnd(Simulation run);
|
||||
|
||||
/**
|
||||
* Ends the simulation when the given node has reached the specified number of
|
||||
@@ -33,7 +33,7 @@ public interface EndCriteria {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldEnd(NetSimulation.SimulationRun run) {
|
||||
public boolean shouldEnd(Simulation run) {
|
||||
return run.getNode(nodeName).stats.numArrivals >= this.maxArrivals;
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,7 @@ public interface EndCriteria {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldEnd(NetSimulation.SimulationRun run) {
|
||||
public boolean shouldEnd(Simulation run) {
|
||||
return run.getNode(nodeName).stats.numDepartures >= this.maxDepartures;
|
||||
}
|
||||
}
|
||||
@@ -82,7 +82,7 @@ public interface EndCriteria {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldEnd(NetSimulation.SimulationRun run) {
|
||||
public boolean shouldEnd(Simulation run) {
|
||||
return run.getTime() >= this.maxTime;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package net.berack.upo.valpre;
|
||||
package net.berack.upo.valpre.sim;
|
||||
|
||||
/**
|
||||
* Represents an event in the simulation.
|
||||
131
src/main/java/net/berack/upo/valpre/sim/Net.java
Normal file
131
src/main/java/net/berack/upo/valpre/sim/Net.java
Normal file
@@ -0,0 +1,131 @@
|
||||
package net.berack.upo.valpre.sim;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import net.berack.upo.valpre.rand.Rng;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
public final class Net {
|
||||
private final HashMap<String, Integer> indices = new HashMap<>();
|
||||
private final List<ServerNode> servers = new ArrayList<>();
|
||||
private final List<List<Connection>> connections = new ArrayList<>();
|
||||
private final List<Double> sum = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Adds a new server node to the network.
|
||||
* The unique identifier for the nodes is the name and, if you try to add a node
|
||||
* that has the same name of another, then the method will return an exception
|
||||
*
|
||||
* @param node The server node to add.
|
||||
* @throws IllegalArgumentException if the node already exist
|
||||
*/
|
||||
public void addNode(ServerNode node) {
|
||||
if (this.indices.containsKey(node.name))
|
||||
throw new IllegalArgumentException("Node already exist");
|
||||
|
||||
this.servers.add(node);
|
||||
this.indices.put(node.name, this.servers.size() - 1);
|
||||
this.connections.add(new ArrayList<>());
|
||||
this.sum.add(0.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a connection between the nodes with the given weight to select it.
|
||||
* The weight must be > 0 and the nodes must be already added to the net.
|
||||
* If the connection is already present then the new weight is used.
|
||||
*
|
||||
* @param parent The parent node.
|
||||
* @param child The child node to add.
|
||||
* @param weight The probability of the child node.
|
||||
* @throws NullPointerException if one of the two nodes are not in the net
|
||||
* @throws IllegalArgumentException if the weight is negative or zero
|
||||
*/
|
||||
public void addConnection(String parent, String child, double weight) {
|
||||
var nodeP = this.indices.get(parent);
|
||||
var nodeC = this.indices.get(child);
|
||||
|
||||
if (weight <= 0)
|
||||
throw new IllegalArgumentException("Weight must be > 0");
|
||||
if (nodeP == nodeC && nodeP == null)
|
||||
throw new NullPointerException("One of the nodes does not exist");
|
||||
|
||||
var list = this.connections.get(nodeP);
|
||||
for (var conn : list) {
|
||||
if (conn.index == nodeC) {
|
||||
conn.weight = weight;
|
||||
return;
|
||||
}
|
||||
}
|
||||
list.add(new Connection(nodeC, weight));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of the nodes in the net
|
||||
*
|
||||
* @return the size of the net
|
||||
*/
|
||||
public int size() {
|
||||
return this.servers.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get one of the child nodes from the parent specified. If the index is out of
|
||||
* bounds then an
|
||||
* exception is thrown. If the node has no child then null is returned;
|
||||
*
|
||||
* @param parent the parent node
|
||||
* @param rng the random number generator used for getting one of the child
|
||||
* @return the resultig node
|
||||
*/
|
||||
public ServerNode getChildOf(ServerNode parent, Rng rng) {
|
||||
var index = this.indices.get(parent.name);
|
||||
var random = rng.random();
|
||||
for (var conn : this.connections.get(index)) {
|
||||
random -= conn.weight / 1.0;
|
||||
if (random <= 0) {
|
||||
return this.servers.get(conn.index);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the weights in each connections so that their sum equals 1.
|
||||
* This method should be called by the user if they have inserted weights that
|
||||
* are not summing to 1 or are unsure.
|
||||
*/
|
||||
public void normalizeWeights() {
|
||||
for (var list : this.connections) {
|
||||
var sum = 0.0d;
|
||||
for (var conn : list)
|
||||
sum += conn.weight;
|
||||
for (var conn : list)
|
||||
conn.weight /= sum;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a consumer to all the nodes. The implementation uses a stream and for
|
||||
* this reason you should consider to make thread safe the consumer.
|
||||
*
|
||||
* @param consumer a function that takes in input a ServerNode
|
||||
*/
|
||||
public void forEachNode(Consumer<ServerNode> consumer) {
|
||||
this.servers.stream().forEach(consumer);
|
||||
}
|
||||
|
||||
public static class Connection {
|
||||
public final int index;
|
||||
public double weight;
|
||||
|
||||
private Connection(int index, double weight) {
|
||||
this.index = index;
|
||||
this.weight = weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
package net.berack.upo.valpre;
|
||||
package net.berack.upo.valpre.sim;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.berack.upo.valpre.rand.Distribution;
|
||||
import net.berack.upo.valpre.rand.Rng;
|
||||
|
||||
@@ -14,8 +12,6 @@ public class ServerNode {
|
||||
public final int maxServers;
|
||||
public final int spawnArrivals;
|
||||
public final Distribution distribution;
|
||||
private final List<NodeChild> children = new ArrayList<>();
|
||||
private double sumProbabilities = 0.0;
|
||||
|
||||
/**
|
||||
* Creates a source node with the given name and distribution.
|
||||
@@ -71,34 +67,6 @@ public class ServerNode {
|
||||
this.spawnArrivals = spawnArrivals;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a child node with the given probability to select it.
|
||||
*
|
||||
* @param node The child node to add.
|
||||
* @param probability The probability of the child node.
|
||||
*/
|
||||
public void addChild(ServerNode node, double probability) {
|
||||
this.children.add(new NodeChild(node, probability));
|
||||
this.sumProbabilities += probability;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a child node based on the given random number generator.
|
||||
*
|
||||
* @param rng The random number generator to use.
|
||||
* @return The child node selected based on the probabilities.
|
||||
*/
|
||||
public ServerNode getChild(Rng rng) {
|
||||
var random = rng.random();
|
||||
for (var child : this.children) {
|
||||
random -= child.probability / this.sumProbabilities;
|
||||
if (random <= 0) {
|
||||
return child.node;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a positive sample from the distribution.
|
||||
* This is useful if you need to generate a positive value from a distribution
|
||||
@@ -125,17 +93,4 @@ public class ServerNode {
|
||||
public boolean shouldSpawnArrival(double numArrivals) {
|
||||
return this.spawnArrivals > numArrivals;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a child node with a probability to select it.
|
||||
*/
|
||||
public static class NodeChild {
|
||||
public final ServerNode node;
|
||||
public final double probability;
|
||||
|
||||
public NodeChild(ServerNode node, double probability) {
|
||||
this.node = node;
|
||||
this.probability = probability;
|
||||
}
|
||||
}
|
||||
}
|
||||
226
src/main/java/net/berack/upo/valpre/sim/Simulation.java
Normal file
226
src/main/java/net/berack/upo/valpre/sim/Simulation.java
Normal file
@@ -0,0 +1,226 @@
|
||||
package net.berack.upo.valpre.sim;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.PriorityQueue;
|
||||
|
||||
import net.berack.upo.valpre.rand.Rng;
|
||||
import net.berack.upo.valpre.sim.stats.NetStatistics;
|
||||
import net.berack.upo.valpre.sim.stats.NetStatistics.Statistics;
|
||||
|
||||
/**
|
||||
* Process an entire run of the simulation.
|
||||
*/
|
||||
public final class Simulation {
|
||||
private final Net net;
|
||||
private final Map<String, NodeBehavior> nodes;
|
||||
private final PriorityQueue<Event> fel;
|
||||
private final EndCriteria[] criterias;
|
||||
private final long timeStartedNano;
|
||||
private final long seed;
|
||||
private final Rng rng;
|
||||
private double time;
|
||||
|
||||
/**
|
||||
* Creates a new run of the simulation with the given nodes and random number
|
||||
* generator.
|
||||
*
|
||||
* @param nodes The nodes in the network.
|
||||
* @param rng The random number generator to use.
|
||||
* @param criterias when the simulation has to end.
|
||||
*/
|
||||
public Simulation(Net net, Rng rng, EndCriteria... criterias) {
|
||||
this.net = net;
|
||||
this.nodes = new HashMap<>();
|
||||
this.fel = new PriorityQueue<>();
|
||||
this.criterias = criterias;
|
||||
this.timeStartedNano = System.nanoTime();
|
||||
this.seed = rng.getSeed();
|
||||
this.rng = rng;
|
||||
this.time = 0.0d;
|
||||
|
||||
// Initial arrivals (if spawned)
|
||||
net.forEachNode(node -> {
|
||||
this.nodes.put(node.name, new NodeBehavior());
|
||||
if (node.shouldSpawnArrival(0))
|
||||
this.addArrival(node);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the simulation until a given criteria is met.
|
||||
*
|
||||
* @return The final statistics the network.
|
||||
*/
|
||||
public NetStatistics.RunResult run() {
|
||||
while (!this.hasEnded())
|
||||
this.processNextEvent();
|
||||
return this.endSimulation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the next event in the future event list.
|
||||
* This method will throw NullPointerException if there are no more events.
|
||||
* You should check if the simulation has ended before calling this method.
|
||||
*
|
||||
* @see #hasEnded()
|
||||
*/
|
||||
public void processNextEvent() {
|
||||
var event = fel.poll();
|
||||
var node = event.node;
|
||||
var behaviour = this.nodes.get(node.name);
|
||||
this.time = event.time;
|
||||
|
||||
switch (event.type) {
|
||||
case ARRIVAL -> {
|
||||
if (behaviour.updateArrival(event.time, node.maxServers))
|
||||
this.addDeparture(node);
|
||||
}
|
||||
case DEPARTURE -> {
|
||||
if (behaviour.updateDeparture(event.time))
|
||||
this.addDeparture(node);
|
||||
|
||||
var next = this.net.getChildOf(node, this.rng);
|
||||
if (next != null) {
|
||||
this.addArrival(next);
|
||||
}
|
||||
if (node.shouldSpawnArrival(behaviour.stats.numArrivals)) {
|
||||
this.addArrival(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends the simulation and returns the statistics of the network.
|
||||
*
|
||||
* @return The statistics of the network.
|
||||
*/
|
||||
public NetStatistics.RunResult endSimulation() {
|
||||
var elapsed = System.nanoTime() - this.timeStartedNano;
|
||||
var nodes = new HashMap<String, Statistics>();
|
||||
for (var entry : this.nodes.entrySet())
|
||||
nodes.put(entry.getKey(), entry.getValue().stats);
|
||||
|
||||
return new NetStatistics.RunResult(this.seed, this.time, elapsed, nodes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current time.
|
||||
*
|
||||
* @return a double representing the current time of the simulation.
|
||||
*/
|
||||
public double getTime() {
|
||||
return this.time;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the node requested by the name passed as a string.
|
||||
*
|
||||
* @param node the name of the node
|
||||
* @return the node
|
||||
*/
|
||||
public NodeBehavior getNode(String node) {
|
||||
return this.getNode(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an arrival event to the future event list. The event is created based
|
||||
* on the given node, and no delay is added.
|
||||
*
|
||||
* @param node The node to create the event for.
|
||||
*/
|
||||
public void addArrival(ServerNode node) {
|
||||
var event = Event.newArrival(node, this.time);
|
||||
fel.add(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a departure event to the future event list. The event is created based
|
||||
* on the given node, and the delay is determined by the node's distribution.
|
||||
*
|
||||
* @param node The node to create the event for.
|
||||
*/
|
||||
public void addDeparture(ServerNode node) {
|
||||
var delay = node.getPositiveSample(this.rng);
|
||||
var event = Event.newDeparture(node, this.time + delay);
|
||||
fel.add(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the simulation has finshed based on the given criteria.
|
||||
*
|
||||
* @return True if the simulation should end, false otherwise.
|
||||
*/
|
||||
public boolean hasEnded() {
|
||||
if (fel.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
for (var c : this.criterias) {
|
||||
if (c.shouldEnd(this)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a summary of the behavior of a server node in the network.
|
||||
* It is used by the simulation to track the number of arrivals and departures,
|
||||
* the maximum queue length, the busy time, and the response time.
|
||||
*/
|
||||
public static class NodeBehavior {
|
||||
public int numServerBusy = 0;
|
||||
public final Statistics stats = new Statistics();
|
||||
private final ArrayDeque<Double> queue = new ArrayDeque<>();
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @param time
|
||||
* @param maxServers
|
||||
* @return
|
||||
*/
|
||||
public boolean updateArrival(double time, int maxServers) {
|
||||
var total = this.stats.averageQueueLength * this.stats.numArrivals;
|
||||
|
||||
this.queue.add(time);
|
||||
this.stats.numArrivals++;
|
||||
this.stats.averageQueueLength = (total + this.queue.size()) / this.stats.numArrivals;
|
||||
this.stats.maxQueueLength = Math.max(this.stats.maxQueueLength, this.queue.size());
|
||||
|
||||
var startDeparture = maxServers > this.numServerBusy;
|
||||
if (startDeparture) {
|
||||
this.numServerBusy++;
|
||||
} else {
|
||||
this.stats.busyTime += time - this.stats.lastEventTime;
|
||||
}
|
||||
|
||||
this.stats.lastEventTime = time;
|
||||
return startDeparture;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @param time
|
||||
* @return
|
||||
*/
|
||||
public boolean updateDeparture(double time) {
|
||||
var startService = this.queue.poll();
|
||||
var response = time - startService;
|
||||
|
||||
var startDeparture = this.queue.size() >= this.numServerBusy;
|
||||
if (!startDeparture) {
|
||||
this.numServerBusy--;
|
||||
}
|
||||
|
||||
this.stats.numDepartures++;
|
||||
this.stats.responseTime += response;
|
||||
this.stats.busyTime += time - this.stats.lastEventTime;
|
||||
this.stats.lastEventTime = time;
|
||||
return startDeparture;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package net.berack.upo.valpre.sim;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import net.berack.upo.valpre.rand.Rng;
|
||||
import net.berack.upo.valpre.rand.Rngs;
|
||||
import net.berack.upo.valpre.sim.stats.NetStatistics;
|
||||
import net.berack.upo.valpre.sim.stats.NetStatistics.RunResult;
|
||||
|
||||
/**
|
||||
* A network simulation that uses a discrete event simulation to model the
|
||||
* behavior of a network of servers.
|
||||
*/
|
||||
public class SimulationMultiple {
|
||||
private final Net net;
|
||||
|
||||
public SimulationMultiple(Net net) {
|
||||
this.net = net;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the simualtion multiple times with the given seed and number of runs.
|
||||
* The runs are calculated one after the other. For a parallel run see
|
||||
* {@link #runParallel(long, int, EndCriteria...)}.
|
||||
*
|
||||
* @param seed The seed to use for the random number generator.
|
||||
* @param runs The number of runs to perform.
|
||||
* @param criterias The criteria to determine when to end the simulation. If
|
||||
* null then the simulation will run until there are no more
|
||||
* events.
|
||||
* @return The statistics the network.
|
||||
*/
|
||||
public NetStatistics run(long seed, int runs, EndCriteria... criterias) {
|
||||
var rng = new Rng(seed);
|
||||
var stats = new RunResult[runs];
|
||||
|
||||
for (int i = 0; i < runs; i++) {
|
||||
var sim = new Simulation(this.net, rng, criterias);
|
||||
stats[i] = sim.run();
|
||||
}
|
||||
return new NetStatistics(stats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the simulation multiple times with the given seed and number of runs.
|
||||
* The runs are calculated in parallel using the given number of threads.
|
||||
* The maximum number of threads are determined by the available processors
|
||||
* and the number of runs.
|
||||
*
|
||||
* @param seed The seed to use for the random number generator.
|
||||
* @param runs The number of runs to perform.
|
||||
* @param criterias The criteria to determine when to end the simulation. If
|
||||
* null then the simulation will run until there are no more
|
||||
* events.
|
||||
* @return The statistics the network.
|
||||
* @throws InterruptedException If the threads are interrupted.
|
||||
* @throws ExecutionException If the one of the threads has been aborted.
|
||||
*/
|
||||
public NetStatistics runParallel(long seed, int runs, EndCriteria... criterias)
|
||||
throws InterruptedException, ExecutionException {
|
||||
var rngs = new Rngs(seed);
|
||||
var results = new NetStatistics.RunResult[runs];
|
||||
var futures = new Future[runs];
|
||||
|
||||
var numThreads = Math.min(runs, Runtime.getRuntime().availableProcessors());
|
||||
try (var threads = Executors.newFixedThreadPool(numThreads)) {
|
||||
for (int i = 0; i < runs; i++) {
|
||||
final var id = i;
|
||||
futures[i] = threads.submit(() -> {
|
||||
var sim = new Simulation(this.net, rngs.getRng(id), criterias);
|
||||
results[id] = sim.run();
|
||||
});
|
||||
}
|
||||
|
||||
for (var i = 0; i < runs; i++) {
|
||||
futures[i].get();
|
||||
}
|
||||
|
||||
return new NetStatistics(results);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package net.berack.upo.valpre.sim.stats;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
public class ConsoleTable {
|
||||
|
||||
private StringBuilder builder = new StringBuilder();
|
||||
private final int maxLen;
|
||||
private final String border;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @param header
|
||||
*/
|
||||
public ConsoleTable(String... header) {
|
||||
var max = 0;
|
||||
for (var name : header)
|
||||
max = Math.max(max, name.length());
|
||||
this.maxLen = max + 2;
|
||||
this.border = ("+" + "═".repeat(maxLen)).repeat(header.length) + "+\n";
|
||||
this.builder.append(border);
|
||||
this.addRow(header);
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @param values
|
||||
*/
|
||||
public void addRow(String... values) {
|
||||
for (var val : values) {
|
||||
var diff = maxLen - val.length();
|
||||
var first = (int) Math.ceil(diff / 2.0);
|
||||
builder.append('║');
|
||||
builder.append(" ".repeat(first));
|
||||
builder.append(val);
|
||||
builder.append(" ".repeat(diff - first));
|
||||
}
|
||||
|
||||
builder.append("║\n");
|
||||
builder.append(border);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package net.berack.upo.valpre;
|
||||
package net.berack.upo.valpre.sim.stats;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@@ -125,7 +125,7 @@ public class NetStatistics {
|
||||
* @param nodes The collection of server nodes to track.
|
||||
* @param rng The random number generator to use.
|
||||
*/
|
||||
public RunResult(long seed, double time, long elapsed, HashMap<String, Statistics> nodes) {
|
||||
public RunResult(long seed, double time, long elapsed, Map<String, Statistics> nodes) {
|
||||
this.seed = seed;
|
||||
this.simulationTime = time;
|
||||
this.timeElapsedNano = elapsed;
|
||||
@@ -227,42 +227,4 @@ public class NetStatistics {
|
||||
this.lastEventTime = 0.0d;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
private static class ConsoleTable {
|
||||
private StringBuilder builder = new StringBuilder();
|
||||
private final int maxLen;
|
||||
private final String border;
|
||||
|
||||
public ConsoleTable(String... header) {
|
||||
var max = 0;
|
||||
for (var name : header)
|
||||
max = Math.max(max, name.length());
|
||||
this.maxLen = max + 2;
|
||||
this.border = ("+" + "═".repeat(maxLen)).repeat(header.length) + "+\n";
|
||||
this.builder.append(border);
|
||||
this.addRow(header);
|
||||
}
|
||||
|
||||
public void addRow(String... values) {
|
||||
for (var val : values) {
|
||||
var diff = maxLen - val.length();
|
||||
var first = (int) Math.ceil(diff / 2.0);
|
||||
builder.append('║');
|
||||
builder.append(" ".repeat(first));
|
||||
builder.append(val);
|
||||
builder.append(" ".repeat(diff - first));
|
||||
}
|
||||
|
||||
builder.append("║\n");
|
||||
builder.append(border);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user