diff --git a/src/main/java/net/berack/upo/valpre/EndSimulationCriteria.java b/src/main/java/net/berack/upo/valpre/EndCriteria.java similarity index 73% rename from src/main/java/net/berack/upo/valpre/EndSimulationCriteria.java rename to src/main/java/net/berack/upo/valpre/EndCriteria.java index c1f1b76..b35c7e5 100644 --- a/src/main/java/net/berack/upo/valpre/EndSimulationCriteria.java +++ b/src/main/java/net/berack/upo/valpre/EndCriteria.java @@ -1,26 +1,22 @@ package net.berack.upo.valpre; -import java.util.Map; - -import net.berack.upo.valpre.NetSimulation.Statistics; - /** * Criteria to determine when to end the simulation. */ -public interface EndSimulationCriteria { +public interface EndCriteria { /** * Determines if the simulation should end based on the statistics of the nodes. * * @param stats The statistics of the nodes in the network. * @return True if the simulation should end, false otherwise. */ - public boolean shouldEnd(Map stats); + public boolean shouldEnd(NetStatistics.SingleRun stats); /** * Ends the simulation when the given node has reached the specified number of * arrivals. */ - public static class MaxArrivals implements EndSimulationCriteria { + public static class MaxArrivals implements EndCriteria { private final String nodeName; private final int maxArrivals; @@ -37,8 +33,8 @@ public interface EndSimulationCriteria { } @Override - public boolean shouldEnd(Map stats) { - return stats.get(nodeName).numArrivals >= this.maxArrivals; + public boolean shouldEnd(NetStatistics.SingleRun stats) { + return stats.nodes.get(nodeName).numArrivals >= this.maxArrivals; } } @@ -46,7 +42,7 @@ public interface EndSimulationCriteria { * Ends the simulation when the given node has reached the specified number of * departures. */ - public static class MaxDepartures implements EndSimulationCriteria { + public static class MaxDepartures implements EndCriteria { private final String nodeName; private final int maxDepartures; @@ -63,8 +59,8 @@ public interface EndSimulationCriteria { } @Override - public boolean shouldEnd(Map stats) { - return stats.get(nodeName).numDepartures >= this.maxDepartures; + public boolean shouldEnd(NetStatistics.SingleRun stats) { + return stats.nodes.get(nodeName).numDepartures >= this.maxDepartures; } } @@ -72,7 +68,7 @@ public interface EndSimulationCriteria { * Ends the simulation when the given node has reached the specified number of * departures. */ - public static class MaxTime implements EndSimulationCriteria { + public static class MaxTime implements EndCriteria { private final double maxTime; /** @@ -86,8 +82,8 @@ public interface EndSimulationCriteria { } @Override - public boolean shouldEnd(Map stats) { - return stats.values().stream().anyMatch(s -> s.lastEventTime >= this.maxTime); + public boolean shouldEnd(NetStatistics.SingleRun stats) { + return stats.simulationTime >= this.maxTime; } } } \ No newline at end of file diff --git a/src/main/java/net/berack/upo/valpre/Event.java b/src/main/java/net/berack/upo/valpre/Event.java index b789d9f..dd8e22e 100644 --- a/src/main/java/net/berack/upo/valpre/Event.java +++ b/src/main/java/net/berack/upo/valpre/Event.java @@ -10,6 +10,7 @@ public class Event implements Comparable { /** * Create a new event. + * * @param type The type of event. * @param node The node that the event is associated with. * @param time The time at which the event occurs. @@ -29,8 +30,22 @@ public class Event implements Comparable { return 1; } + /** + * Create a new event. + * + * @param node The node that the event is associated with. + * @param time The time at which the event occurs. + * @param type The type of event. + * + * @return The new event. + */ + public static Event newType(ServerNode node, double time, Type type) { + return new Event(type, node, time); + } + /** * Create a new arrival event. + * * @param node The node that the event is associated with. * @param time The time at which the event occurs. * @return The new event. @@ -41,6 +56,7 @@ public class Event implements Comparable { /** * Create a new departure event. + * * @param node The node that the event is associated with. * @param time The time at which the event occurs. * @return The new event. diff --git a/src/main/java/net/berack/upo/valpre/Main.java b/src/main/java/net/berack/upo/valpre/Main.java index bf5de97..e86fc21 100644 --- a/src/main/java/net/berack/upo/valpre/Main.java +++ b/src/main/java/net/berack/upo/valpre/Main.java @@ -3,7 +3,7 @@ package net.berack.upo.valpre; import net.berack.upo.valpre.rand.Distribution; public class Main { - public static void main(String[] args) { + public static void main(String[] args) throws Exception { // Parameters for the simulation var seed = System.nanoTime(); var total = 100000; @@ -17,28 +17,13 @@ public class Main { node1.addChild(node2, 1.0); /// Run the simulation - var sim = new NetSimulation(seed); + var sim = new NetSimulation(); sim.addNode(node1); sim.addNode(node2); - var maxDepartures = new EndSimulationCriteria.MaxDepartures("Queue", total); - //var maxTime = new EndSimulationCriteria.MaxTime(1000.0); - var results = sim.run(maxDepartures); - - // Display the results - for (var entry : results.entrySet()) { - var stats = entry.getValue(); - var size = (int) Math.ceil(Math.max(Math.log10(stats.numArrivals), Math.log10(stats.lastEventTime))); - var iFormat = "%" + size + "d"; - var fFormat = "%" + (size + 4) + ".3f"; - - System.out.println("===== " + entry.getKey() + " ====="); - System.out.printf(" Arrivals: \t" + iFormat + "\n", stats.numArrivals); - System.out.printf(" Departures:\t" + iFormat + "\n", stats.numDepartures); - System.out.printf(" Max Queue: \t" + iFormat + "\n", stats.maxQueueLength); - System.out.printf(" Response: \t" + fFormat + "\n", stats.responseTime / stats.numDepartures); - System.out.printf(" Busy %%: \t" + fFormat + "\n", stats.busyTime * 100 / stats.lastEventTime); - System.out.printf(" Last Event:\t" + fFormat + "\n", stats.lastEventTime); - } + // var maxDepartures = new EndSimulationCriteria.MaxDepartures("Queue", total); + // var maxTime = new EndSimulationCriteria.MaxTime(1000.0); + var results = sim.runParallel(seed, 100); + results.runs[80].printSummary(); } } \ No newline at end of file diff --git a/src/main/java/net/berack/upo/valpre/NetSimulation.java b/src/main/java/net/berack/upo/valpre/NetSimulation.java index 28ae3a2..fae6713 100644 --- a/src/main/java/net/berack/upo/valpre/NetSimulation.java +++ b/src/main/java/net/berack/upo/valpre/NetSimulation.java @@ -1,27 +1,22 @@ package net.berack.upo.valpre; -import java.util.ArrayDeque; -import java.util.HashMap; -import java.util.Map; +import java.util.ArrayList; +import java.util.Collection; 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.SingleRun; 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 { - public final long seed; - private final Map servers = new HashMap<>(); - - /** - * Creates a new network simulation with the given seed. - * - * @param seed The seed to use for the random number generator. - */ - public NetSimulation(long seed) { - this.seed = seed; - } + private final Collection servers = new ArrayList<>(); /** * Adds a new server node to the network. @@ -29,90 +24,152 @@ public class NetSimulation { * @param node The server node to add. */ public void addNode(ServerNode node) { - this.servers.put(node.name, node); + this.servers.add(node); } /** - * Runs the simulation for the given number of total arrivals, stopping when the - * given node has reached the specified number of departures. - * If needed the run method can be called by multiple threads. + * Runs the simulation with the given seed until a given criteria is met. * - * @param criteria 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 of the nodes in the network. + * @param seed The seed to use for the random number generator. + * @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 Map run(EndSimulationCriteria... criteria) { - // Initialization - var timeNow = 0.0d; - var rng = new Rng(this.seed); // TODO change here for thread variance (use Rngs with ids) - var fel = new PriorityQueue(); - var stats = new HashMap(); - for (var node : this.servers.values()) { - var s = new Statistics(rng); - s.addArrivalIf(node.shouldSpawnArrival(s.numArrivals), node, timeNow, fel); - stats.put(node.name, s); + public NetStatistics.SingleRun run(long seed, EndCriteria... criterias) { + return this.run(new Rng(seed), criterias); + } + + /** + * 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 SingleRun[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. + * + * @param seed The seed to use for the random number generator. + * @param runs The number of runs to perform. + * @param numThreads The number of threads to use for the simulation. + * @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 stats = new NetStatistics.SingleRun[runs]; + var futures = new Future[runs]; + + var numThreads = Math.min(runs, Runtime.getRuntime().availableProcessors()); + var threads = Executors.newFixedThreadPool(numThreads); + + for (int i = 0; i < runs; i++) { + final var id = i; + futures[i] = threads.submit(() -> { + stats[id] = this.run(rngs.getRng(id), criterias); + }); } - // Main Simulation Loop - while (!fel.isEmpty() && !this.shouldEnd(criteria, stats)) { + for (var i = 0; i < runs; i++) { + futures[i].get(); + } + + threads.shutdownNow(); + return new NetStatistics(stats); + } + + /** + * 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.SingleRun run(Rng rng, EndCriteria... criterias) { + var run = new SimpleRun(this.servers, rng, criterias); + while (!run.hasEnded()) { + run.processNextEvent(); + } + return run.endSimulation(); + } + + /** + * Process an entire run of the simulation. + */ + public static class SimpleRun { + + private final NetStatistics.SingleRun stats; + private final PriorityQueue fel; + private final EndCriteria[] criterias; + + /** + * 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. + */ + private SimpleRun(Collection nodes, Rng rng, EndCriteria... criterias) { + this.fel = new PriorityQueue<>(); + this.stats = new NetStatistics.SingleRun(nodes, rng); + this.criterias = criterias; + + // Initial arrivals (if spawned) + for (var node : nodes) { + if (node.shouldSpawnArrival(0)) + this.addEvent(node, Event.Type.ARRIVAL); + } + } + + /** + * 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 statsNode = stats.get(event.node.name); - timeNow = event.time; + stats.simulationTime = event.time; switch (event.type) { - case ARRIVAL -> statsNode.processArrival(event, timeNow, fel); - case DEPARTURE -> statsNode.processDeparture(event, timeNow, fel); + case ARRIVAL -> this.processArrival(event); + case DEPARTURE -> this.processDeparture(event); } } - return stats; - } - - private boolean shouldEnd(EndSimulationCriteria[] criteria, Map stats) { - for (var c : criteria) { - if (c.shouldEnd(stats)) { - return true; - } - } - return false; - } - - /** - * Represents a statistical 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 Statistics { - public int numArrivals = 0; - public int numDepartures = 0; - public int maxQueueLength = 0; - public double busyTime = 0.0; - public double responseTime = 0.0; - public double lastEventTime = 0.0; - - private int numServerBusy = 0; - private ArrayDeque queue = new ArrayDeque<>(); - private final Rng rng; /** - * Creates a new statistics object with the given random number generator. + * Ends the simulation and returns the statistics of the network. * - * @param rng The random number generator to use. + * @return The statistics of the network. */ - public Statistics(Rng rng) { - this.rng = rng; - } - - /** - * Resets the statistics to their initial values. - */ - public void reset() { - this.numArrivals = 0; - this.numDepartures = 0; - this.numServerBusy = 0; - this.busyTime = 0.0; - this.responseTime = 0.0; - this.queue.clear(); + public NetStatistics.SingleRun endSimulation() { + this.stats.endSimulation(); + return this.stats; } /** @@ -122,26 +179,26 @@ public class NetSimulation { * arrival. If a server is available, a departure event is created and added to * the future event list. * - * @param event The arrival event to process. - * @param timeNow The current time of the simulation. - * @param fel The future event list to add new events to. + * @param stats The statistics of the network. + * @param event The arrival event to process. + * @param fel The future event list to add new events to. */ - private void processArrival(Event event, double timeNow, PriorityQueue fel) { - this.numArrivals++; - this.queue.add(event.time); - this.maxQueueLength = Math.max(this.maxQueueLength, this.queue.size()); + private void processArrival(Event event) { + var nodeStats = stats.nodes.get(event.node.name); - if (event.node.maxServers > this.numServerBusy) { - this.numServerBusy++; - var time = event.node.getPositiveSample(this.rng); - var departure = Event.newDeparture(event.node, timeNow + time); - fel.add(departure); + nodeStats.numArrivals++; + nodeStats.enqueue(event.time); + if (event.node.maxServers > nodeStats.numServerBusy) { + nodeStats.numServerBusy++; + this.addEvent(event.node, Event.Type.DEPARTURE); } else { - this.busyTime += timeNow - this.lastEventTime; + nodeStats.busyTime += stats.simulationTime - nodeStats.lastEventTime; } - this.lastEventTime = timeNow; - this.addArrivalIf(event.node.shouldSpawnArrival(this.numArrivals), event.node, timeNow, fel); + nodeStats.lastEventTime = stats.simulationTime; + if (event.node.shouldSpawnArrival(nodeStats.numArrivals)) { + this.addEvent(event.node, Event.Type.ARRIVAL); + } } /** @@ -152,45 +209,63 @@ public class NetSimulation { * At the end it will add an arrival to the next node if the current node has a * child. * - * @param event The departure event to process. - * @param timeNow The current time of the simulation. - * @param fel The future event list to add new events to. + * @param stats The statistics of the network. + * @param event The departure event to process. + * @param fel The future event list to add new events to. */ - private void processDeparture(Event event, double timeNow, PriorityQueue fel) { - var startService = this.queue.poll(); - var response = timeNow - startService; + private void processDeparture(Event event) { + var nodeStats = stats.nodes.get(event.node.name); + var startService = nodeStats.dequeue(); + var response = stats.simulationTime - startService; - if (this.queue.size() < this.numServerBusy) { - this.numServerBusy--; + if (nodeStats.getQueueSize() < nodeStats.numServerBusy) { + nodeStats.numServerBusy--; } else { - var time = event.node.getPositiveSample(this.rng); - var departure = Event.newDeparture(event.node, timeNow + time); - fel.add(departure); + this.addEvent(event.node, Event.Type.DEPARTURE); } - this.numDepartures++; - this.responseTime += response; - this.busyTime += timeNow - this.lastEventTime; - this.lastEventTime = timeNow; + nodeStats.numDepartures++; + nodeStats.responseTime += response; + nodeStats.busyTime += stats.simulationTime - nodeStats.lastEventTime; + nodeStats.lastEventTime = stats.simulationTime; - var next = event.node.getChild(rng); - this.addArrivalIf(!event.node.shouldSinkDeparture(this.numDepartures), next, timeNow, fel); + if (!event.node.shouldSinkDeparture(nodeStats.numDepartures)) { + var next = event.node.getChild(stats.rng); + this.addEvent(next, Event.Type.ARRIVAL); + } } /** - * Adds an arrival event to the future event list if the given condition is - * true. + * Adds an event to the future event list. + * The event is created based on the given node and type, and the delay is + * determined by the node's distribution. * - * @param condition The condition to check. - * @param node The node to add the arrival event for. - * @param timeNow The current time of the simulation. - * @param fel The future event list to add the event to. + * @param node The node to create the event for. + * @param type The type of event to create. */ - private void addArrivalIf(boolean condition, ServerNode node, double timeNow, PriorityQueue fel) { - if (condition && node != null) { - var delay = node.getPositiveSample(this.rng); - fel.add(Event.newArrival(node, timeNow + delay)); + public void addEvent(ServerNode node, Event.Type type) { + if (node != null) { + var delay = node.getPositiveSample(stats.rng); + var event = Event.newType(node, stats.simulationTime + delay, type); + 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(stats)) { + return true; + } + } + return false; + } } } diff --git a/src/main/java/net/berack/upo/valpre/NetStatistics.java b/src/main/java/net/berack/upo/valpre/NetStatistics.java new file mode 100644 index 0000000..807777b --- /dev/null +++ b/src/main/java/net/berack/upo/valpre/NetStatistics.java @@ -0,0 +1,139 @@ +package net.berack.upo.valpre; + +import java.util.ArrayDeque; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import net.berack.upo.valpre.rand.Rng; + +public class NetStatistics { + public final SingleRun[] runs; + + public NetStatistics(SingleRun... runs) { + this.runs = runs; + } + + /** + * Represents the statistics of a network simulation. + * It is used by the simulation to track the behavior of the network and its + * nodes, including the number of arrivals and departures, the maximum queue + * length, the busy time, and the response time. + */ + public static class SingleRun { + public final Map nodes; + public final long seed; + public final Rng rng; + public double simulationTime; + public long timeElapsedNano; + + /** + * Creates a new statistics object for the given collection of server nodes and + * random number generator. + * + * @param nodes The collection of server nodes to track. + * @param rng The random number generator to use. + */ + public SingleRun(Collection nodes, Rng rng) { + this.rng = rng; + this.seed = rng.getSeed(); + + this.simulationTime = 0.0d; + this.timeElapsedNano = System.nanoTime(); + this.nodes = new HashMap(); + for (var node : nodes) { + var s = new Node(); + this.nodes.put(node.name, s); + } + + } + + /** + * Ends the simulation and calculates the elapsed time. + */ + public void endSimulation() { + this.timeElapsedNano = System.nanoTime() - this.timeElapsedNano; + } + + /** + * Print a summary of the statistics to the console. + * The summary includes the seed, the simulation time, the elapsed time, and + * the statistics for each node in the network. + */ + public void printSummary() { + var size = (int) Math.ceil(Math.log10(this.simulationTime)); + var format = "%" + (size + 4) + ".3f"; + + System.out.println("===== Net Stats ====="); + System.out.println("Seed: \t" + this.seed); + System.out.printf("Simulation: \t" + format + "\n", this.simulationTime); + System.out.printf("Elapsed: \t" + format + "ms\n", this.timeElapsedNano / 1e6); + + for (var entry : this.nodes.entrySet()) { + var stats = entry.getValue(); + var entrySize = (int) Math.max(size, (int) Math.ceil((Math.log10(stats.numArrivals)))); + var iFormat = "%" + entrySize + "d"; + var fFormat = "%" + (entrySize + 4) + ".3f"; + + System.out.println("===== " + entry.getKey() + " ====="); + System.out.printf(" Arrivals: \t" + iFormat + "\n", stats.numArrivals); + System.out.printf(" Departures:\t" + iFormat + "\n", stats.numDepartures); + System.out.printf(" Max Queue: \t" + iFormat + "\n", stats.maxQueueLength); + System.out.printf(" Response: \t" + fFormat + "\n", stats.responseTime / stats.numDepartures); + System.out.printf(" Busy %%: \t" + fFormat + "\n", stats.busyTime * 100 / stats.lastEventTime); + System.out.printf(" Last Event:\t" + fFormat + "\n", stats.lastEventTime); + } + } + } + + /** + * Represents a statistical 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 Node { + public int numArrivals = 0; + public int numDepartures = 0; + public int maxQueueLength = 0; + public double averageQueueLength = 0.0d; + public double busyTime = 0.0d; + public double responseTime = 0.0d; + public double lastEventTime = 0.0d; + + public int numServerBusy = 0; + private ArrayDeque queue = new ArrayDeque<>(); + + /** + * Resets the statistics to their initial values. + */ + public void reset() { + this.numArrivals = 0; + this.numDepartures = 0; + this.maxQueueLength = 0; + this.averageQueueLength = 0.0; + this.busyTime = 0.0; + this.responseTime = 0.0; + this.lastEventTime = 0.0; + this.numServerBusy = 0; + this.queue.clear(); + } + + public double dequeue() { + return this.queue.poll(); + } + + public void enqueue(double time) { + var total = this.averageQueueLength * (this.numArrivals - 1); + + this.queue.add(time); + this.averageQueueLength = (total + this.queue.size()) / this.numArrivals; + this.maxQueueLength = Math.max(this.maxQueueLength, this.queue.size()); + } + + public int getQueueSize() { + return this.queue.size(); + } + } + +} \ No newline at end of file diff --git a/src/main/java/net/berack/upo/valpre/rand/Rngs.java b/src/main/java/net/berack/upo/valpre/rand/Rngs.java index 691248f..0a591e7 100644 --- a/src/main/java/net/berack/upo/valpre/rand/Rngs.java +++ b/src/main/java/net/berack/upo/valpre/rand/Rngs.java @@ -33,7 +33,7 @@ package net.berack.upo.valpre.rand; * Language : Java * Latest Revision : 6-10-04 */ -class Rngs { +public class Rngs { private final static int STREAMS = 256; /* # of streams, DON'T CHANGE THIS VALUE */ private final static long A256 = 22925; /* jump multiplier, DON'T CHANGE THIS VALUE */