Browse Source

More on the simulator

It's just missing churning of clients and relays at the end of each
epoch
Ian Goldberg 4 years ago
parent
commit
806ad38bc0
2 changed files with 184 additions and 7 deletions
  1. 54 0
      network.py
  2. 130 7
      simulator.py

+ 54 - 0
network.py

@@ -3,6 +3,7 @@
 import random
 import pickle
 import logging
+import math
 from enum import Enum
 
 # Set this to True if you want the bytes sent and received to be added
@@ -85,6 +86,59 @@ class PerfStats:
         self.dhs = 0
 
 
+class PerfStatsStats:
+    """Accumulate a number of PerfStats objects to compute the means and
+    stddevs of their fields."""
+
+    class SingleStat:
+        """Accumulate single numbers to compute their mean and
+        stddev."""
+
+        def __init__(self):
+            self.tot = 0
+            self.totsq = 0
+            self.N = 0
+
+        def accum(self, x):
+            self.tot += x
+            self.totsq += x*x
+            self.N += 1
+
+        def __str__(self):
+            mean = self.tot/self.N
+            stddev = math.sqrt((self.totsq - self.tot*self.tot/self.N) \
+                    / (self.N - 1))
+            return "%f \pm %f" % (mean, stddev)
+
+    def __init__(self):
+        self.bytes_sent = PerfStatsStats.SingleStat()
+        self.bytes_received = PerfStatsStats.SingleStat()
+        self.bytes_tot = PerfStatsStats.SingleStat()
+        self.keygens = PerfStatsStats.SingleStat()
+        self.sigs = PerfStatsStats.SingleStat()
+        self.verifs = PerfStatsStats.SingleStat()
+        self.dhs = PerfStatsStats.SingleStat()
+        self.N = 0
+
+    def accum(self, stat):
+        self.bytes_sent.accum(stat.bytes_sent)
+        self.bytes_received.accum(stat.bytes_received)
+        self.bytes_tot.accum(stat.bytes_sent + stat.bytes_received)
+        self.keygens.accum(stat.keygens)
+        self.sigs.accum(stat.sigs)
+        self.verifs.accum(stat.verifs)
+        self.dhs.accum(stat.dhs)
+        self.N += 1
+
+    def __str__(self):
+        if self.N > 0:
+            return "sent=%s recv=%s bytes=%s keygen=%s sig=%s verif=%s dh=%s N=%s" % \
+                    (self.bytes_sent, self.bytes_received, self.bytes_tot,
+                    self.keygens, self.sigs, self.verifs, self.dhs, self.N)
+        else:
+            return "None"
+
+
 class NetAddr:
     """A class representing a network address"""
     nextaddr = 1

+ 130 - 7
simulator.py

@@ -12,9 +12,10 @@ import relay
 import client
 
 class Simulator:
-    def __init__(self, relaytarget, clienttarget):
+    def __init__(self, relaytarget, clienttarget, statslogger):
         self.relaytarget = relaytarget
         self.clienttarget = clienttarget
+        self.statslogger = statslogger
 
         # Some (for now) hard-coded parameters
 
@@ -24,6 +25,9 @@ class Simulator:
         # The fraction of relays that are fallback relays
         fracfallbackrelays = 0.05
 
+        # Mean number of circuits created per client per epoch
+        self.gamma = 8.9
+
         # Start some dirauths
         self.dirauthaddrs = []
         self.dirauths = []
@@ -59,14 +63,119 @@ class Simulator:
         for i in range(clienttarget):
             self.clients.append(client.Client(self.dirauthaddrs))
 
+        # Throw away all the performance statistics to this point
+        for d in self.dirauths: d.perfstats.reset()
+        for r in self.relays: r.perfstats.reset()
+        for c in self.clients: c.perfstats.reset()
+
         # Tick the epoch to bootstrap the clients
         network.thenetwork.nextepoch()
 
-        # Throw away all the performance statistics to this point
+    def one_epoch(self):
+        """Simulate one epoch."""
+
+        epoch = network.thenetwork.getepoch()
+
+        # Each client will start a random number of circuits in a
+        # Poisson distribution with mean gamma.  To randomize the order
+        # of the clients creating each circuit, we actually use a
+        # Poisson distribution with mean (gamma*num_clients), and assign
+        # each event to a uniformly random client.  (This does in fact
+        # give the required distribution.)
+
+        numclients = len(self.clients)
+
+        # simtime is the simulated time, measured in epochs (i.e.,
+        # 0=start of this epoch; 1=end of this epoch)
+        simtime = 0
+
+        allcircs = []
+
+        lastpercent = -1
+        while simtime < 1.0:
+            allcircs.append(
+                    random.choice(self.clients).channelmgr.new_circuit())
+            simtime += random.expovariate(self.gamma * numclients)
+            percent = int(100*simtime)
+            if percent != lastpercent:
+                logging.info("Creating circuits in epoch %s: %d%%",
+                        epoch, percent)
+                lastpercent = percent
+
+        # gather stats
+        totsent = 0
+        totrecv = 0
+        dirasent = 0
+        dirarecv = 0
+        relaysent = 0
+        relayrecv = 0
+        clisent = 0
+        clirecv = 0
+        dirastats = network.PerfStatsStats()
+        for d in self.dirauths:
+            logging.debug("%s", d.perfstats)
+            dirasent += d.perfstats.bytes_sent
+            dirarecv += d.perfstats.bytes_received
+            dirastats.accum(d.perfstats)
+        totsent += dirasent
+        totrecv += dirarecv
+        relaystats = network.PerfStatsStats()
+        relaybstats = network.PerfStatsStats()
+        relaynbstats = network.PerfStatsStats()
+        for r in self.relays:
+            logging.debug("%s", r.perfstats)
+            relaysent += r.perfstats.bytes_sent
+            relayrecv += r.perfstats.bytes_received
+            relaystats.accum(r.perfstats)
+            if r.perfstats.is_bootstrapping:
+                relaybstats.accum(r.perfstats)
+            else:
+                relaynbstats.accum(r.perfstats)
+        totsent += relaysent
+        totrecv += relayrecv
+        clistats = network.PerfStatsStats()
+        clibstats = network.PerfStatsStats()
+        clinbstats = network.PerfStatsStats()
+        for c in self.clients:
+            logging.debug("%s", c.perfstats)
+            clisent += c.perfstats.bytes_sent
+            clirecv += c.perfstats.bytes_received
+            clistats.accum(c.perfstats)
+            if c.perfstats.is_bootstrapping:
+                clibstats.accum(c.perfstats)
+            else:
+                clinbstats.accum(c.perfstats)
+        totsent += clisent
+        totrecv += clirecv
+        self.statslogger.info("DirAuths sent=%s recv=%s" % (dirasent, dirarecv))
+        self.statslogger.info("Relays sent=%s recv=%s" % (relaysent, relayrecv))
+        self.statslogger.info("Client sent=%s recv=%s" % (clisent, clirecv))
+        self.statslogger.info("Total sent=%s recv=%s" % (totsent, totrecv))
+        numdirauths = len(self.dirauths)
+        numrelays = len(self.relays)
+        numclients = len(self.clients)
+        self.statslogger.info("Dirauths %s", dirastats)
+        self.statslogger.info("Relays %s", relaystats)
+        self.statslogger.info("Relays(B) %s", relaybstats)
+        self.statslogger.info("Relays(NB) %s", relaynbstats)
+        self.statslogger.info("Clients %s", clistats)
+        self.statslogger.info("Clients(B) %s", clibstats)
+        self.statslogger.info("Clients(NB) %s", clinbstats)
+
+        # Close circuits
+        for c in allcircs:
+            c.close()
+
+        # Reset stats
         for d in self.dirauths: d.perfstats.reset()
         for r in self.relays: r.perfstats.reset()
         for c in self.clients: c.perfstats.reset()
 
+        # TODO: churn relays and clients
+
+        # Tick the epoch
+        network.thenetwork.nextepoch()
+
 
 if __name__ == '__main__':
     # Args: womode snipauthmode networkscale numepochs randseed
@@ -79,15 +188,27 @@ if __name__ == '__main__':
     networkscale = float(sys.argv[3])
     numepochs = int(sys.argv[4])
     randseed = int(sys.argv[5])
+    logfile = "%s_%s_%f_%s_%s.log" % (womode.name, snipauthmode.name,
+        networkscale, numepochs, randseed)
 
     # Seed the PRNG.  On Ubuntu 18.04, this in fact makes future calls
     # to (non-cryptographic) random numbers deterministic.  On Ubuntu
     # 16.04, it does not.
     random.seed(randseed)
 
+    loglevel = logging.INFO
     # Uncomment to see all the debug messages
-    # logging.basicConfig(level=logging.DEBUG)
-    logging.basicConfig(level=logging.INFO)
+    # loglevel = logging.DEBUG
+
+    logging.basicConfig(level=loglevel,
+            format="%(asctime)s:%(levelname)s:%(message)s")
+
+    # The gathered statistics get logged separately
+    statslogger = logging.getLogger("simulator")
+    handler = logging.FileHandler(logfile)
+    handler.setFormatter(logging.Formatter("%(asctime)s:%(message)s"))
+    statslogger.addHandler(handler)
+    statslogger.setLevel(logging.INFO)
 
     # Set the Walking Onions style to use
     network.thenetwork.set_wo_style(womode, snipauthmode)
@@ -97,9 +218,11 @@ if __name__ == '__main__':
     clienttarget = math.ceil(2500000 * networkscale)
 
     # Create the simulation
-    simulator = Simulator(relaytarget, clienttarget)
+    simulator = Simulator(relaytarget, clienttarget, statslogger)
 
-    network.thenetwork.nextepoch()
+    for e in range(numepochs):
+        statslogger.info("Starting epoch %s simulation", e+3)
+        simulator.one_epoch()
 
     maxmemmib = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024
-    logging.info("%d MiB used", maxmemmib)
+    statslogger.info("%d MiB used", maxmemmib)