[CalendarServer-changes] [15081] CalendarServer/branches/users/sredmond/clientsim/contrib/ performance/loadtest

source_changes at macosforge.org source_changes at macosforge.org
Mon Aug 31 13:37:45 PDT 2015


Revision: 15081
          http://trac.calendarserver.org//changeset/15081
Author:   sredmond at apple.com
Date:     2015-08-31 13:37:45 -0700 (Mon, 31 Aug 2015)
Log Message:
-----------
Adds Python configuration and a lot more

Modified Paths:
--------------
    CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/ampsim.py
    CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/config.py
    CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/distributions.py
    CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/ical.py
    CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/logger.py
    CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/population.py
    CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/profiles.py
    CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/push.py
    CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/records.py
    CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/requester.py
    CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/resources.py
    CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/clients.plist
    CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/settings/clients.py
    CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/settings/config.py
    CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/sim.py
    CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/tests/test_distributions.py
    CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/tests/test_push.py
    CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/tests/test_resources.py

Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/ampsim.py
===================================================================
--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/ampsim.py	2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/ampsim.py	2015-08-31 20:37:45 UTC (rev 15081)
@@ -46,7 +46,6 @@
             exit(0)
     runmain()
 
-
 from copy import deepcopy
 
 from plistlib import writePlistToString, readPlistFromString
@@ -58,14 +57,16 @@
 
 from contrib.performance.loadtest.sim import LoadSimulator
 from contrib.performance.loadtest.records import DirectoryRecord
+from contrib.performance.loadtest.config import Config
 
 class Configure(Command):
     """
     Configure this worker process with the text of an XML property list.
     """
-    arguments = [("plist", String())]
+    arguments = [("cfg", Pickle())]
     # Pass OSError exceptions through, presenting the exception message to the user.
-    errors = {OSError: 'OSError'}
+    # errors = {OSError: 'OSError'}
+    errors = {Exception: 'Exception'}
 
 
 
@@ -112,11 +113,16 @@
 
 
     @Configure.responder
-    def config(self, plist):
+    def config(self, cfg):
         from sys import stderr
-        cfg = readPlistFromString(plist)
+        # cfg = readPlistFromString(plist)
+        config = Config.deserializeFromWorker(cfg)
+        with open('logs.txt', 'a') as f:
+            f.write('here')
+            f.write(str(config.__dict__))
+
         addObserver(self.emit)
-        sim = LoadSimulator.fromConfig(cfg)
+        sim = LoadSimulator.fromConfigObject(config)
         sim.records = self.records
         sim.attachServices(stderr)
         return {}
@@ -161,26 +167,29 @@
                             email=record.email,
                             guid=record.guid)
 
-        workerConfig = deepcopy(self.loadsim.configTemplate)
-        # The list of workers is for the manager only; the workers themselves
-        # know they're workers because they _don't_ receive this list.
-        del workerConfig["workers"]
-        # The manager loads the accounts via the configured loader, then sends
-        # them out to the workers (right above), which look at the state at an
-        # instance level and therefore don't need a globally-named directory
-        # record loader.
-        del workerConfig["accounts"]
+        workerConfig = self.loadsim.configTemplate(self.whichWorker, self.numWorkers)
+        dupe = deepcopy(workerConfig)
+        del dupe['records']
+        print dupe
+        # # The list of workers is for the manager only; the workers themselves
+        # # know they're workers because they _don't_ receive this list.
+        # del workerConfig["workers"]
+        # # The manager loads the accounts via the configured loader, then sends
+        # # them out to the workers (right above), which look at the state at an
+        # # instance level and therefore don't need a globally-named directory
+        # # record loader.
+        # del workerConfig["accounts"]
 
-        workerConfig["workerID"] = self.whichWorker
-        workerConfig["workerCount"] = self.numWorkers
-        workerConfig["observers"] = []
-        workerConfig.pop("accounts", None)
+        # workerConfig["workerID"] = self.whichWorker
+        # workerConfig["workerCount"] = self.numWorkers
+        # workerConfig["observers"] = []
+        # workerConfig.pop("accounts", None)
 
-        plist = writePlistToString(workerConfig)
+        # plist = writePlistToString(workerConfig)
         self.output.write("Initiating worker configuration\n")
         def completed(x):
             self.output.write("Worker configuration complete.\n")
-        self.callRemote(Configure, plist=plist).addCallback(completed)
+        self.callRemote(Configure, cfg=workerConfig).addCallback(completed)
 
 
     @LogMessage.responder

Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/config.py
===================================================================
--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/config.py	2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/config.py	2015-08-31 20:37:45 UTC (rev 15081)
@@ -1,69 +1,127 @@
 from importlib import import_module
 
+from twisted.python.log import msg
 from contrib.performance.loadtest.logger import ReportStatistics, RequestLogger, OperationLogger
-from contrib.performance.loadtest.sim import recordsFromCSVFile
+from contrib.performance.loadtest.records import recordsFromCSVFile
+from contrib.performance.loadtest.population import ClientFactory, PopulationParameters
 
-DEFAULTS = {
-    server = "https://127.0.0.1:8443"
+class Config(object):
+    def __init__(self):
+        pass
 
-    accounts = recordsFromCSVFile("contrib/performance/loadtest/accounts.csv")
+    def populateFrom(self, serverConfig, clientConfig, usePlist=False):
+        # If there is a list of workers, then this process is *not* a worker
+        isManager = serverConfig.get('workers') is not None
 
-    _requestLogger = RequestLogger()
-    _operationLogger = OperationLogger(
-        thresholdsPath="contrib/performance/loadtest/thresholds.json",
-        lagCutoff=1.0,
-        failCutoff=1.0
-    )
-    _statisticsReporter = ReportStatistics(
-        thresholdsPath="contrib/performance/loadtest/thresholds.json",
-        benchmarksPath="contrib/performance/loadtest/benchmarks.json",
-        failCutoff=1.0
-    )
+        if usePlist:
+            # If the supplied files are plists, we need to convert the named objects into real Python objects.
+            # The ensuing hacky code is why I recommend we remove support for plist-based configuration
+            workers = config['workers']
+            if not isManager:
+                # Client / place where the simulator actually runs configuration
+                workerID = config.get("workerID", 0)
+                workerCount = config.get("workerCount", 1)
+                configTemplate = None
+                server = config.get('server', 'http://127.0.0.1:8008')
+                serializationPath = None
 
-    arrival = SmoothRampUp(
-        groups=2,
-        groupSize=1,
-        interval=3,
-        clientsPerUser=1
-    )
-}
+                serializationPath = config['serializationPath']
 
-class Config(object):
+                if 'arrival' in config:
+                    arrival = Arrival(
+                        namedAny(config['arrival']['factory']),
+                        config['arrival']['params'])
+                else:
+                    arrival = Arrival(
+                        SmoothRampUp, dict(groups=10, groupSize=1, interval=3))
 
+                parameters = PopulationParameters()
+                if 'clients' in config:
+                    for clientConfig in config['clients']:
+                        parameters.addClient(
+                            clientConfig["weight"],
+                            ClientType(
+                                clientConfig["software"],
+                                clientConfig["params"],
+                                clientConfig["profiles"]
+                            )
+                        )
+                            # ClientType(
+                            #     namedAny(clientConfig["software"]),
+                            #     cls._convertParams(clientConfig["params"]),
+                            #     [
+                            #         ProfileType(
+                            #             namedAny(profile["class"]),
+                            #             cls._convertParams(profile["params"])
+                            #         ) for profile in clientConfig["profiles"]
+                            #     ]))
+                if not parameters.clients:
+                    parameters.addClient(1,
+                                         ClientType(OS_X_10_6, {},
+                                                    [Eventer, Inviter, Accepter]))
+            else:
+                # Manager / observer process.
+                server = ''
+                serializationPath = None
+                arrival = None
+                parameters = None
+                workerID = 0
+                configTemplate = config
+                workerCount = 1
 
-    def __init__(self, serverConfigFile, clientConfigFile):
-        # These are modules
-        serverConfigModule = import_module(serverConfigFile)
-        clientConfigModule = import_module(clientConfigFile)
+            # webadminPort = 
+            webadminPort = None
+            if 'webadmin' in config:
+                if config['webadmin']['enabled']:
+                    webadminPort = config['webadmin']['HTTPPort']
 
-        self.clients = clientConfigModule.clientConfiguration
-        self.workers = workers
-        self.configTemplate = configTemplate
-        self.workerID = workerID
-        self.workerCount = workerCount
+            serverStats = None
+            if 'serverStats' in config:
+                if config['serverStats']['enabled']:
+                    serverStats = config['serverStats']
+                    serverStats['server'] = config['server'] if 'server' in config else ''
 
-        self.server = serverConfig.get('server')
-        self.webadminPort = serverConfig.get('webadminPort')
-        self.serverStats = serverConfig.get('serverStatsPort')
-        self.serializationPath = serverConfig.get('serializationPath')
-        self.arrival = serverConfig.get('arrival')
-        self.observers = serverConfig.get('observers')
-        self.records = serverConfig.get('records')
-        self.workers = serverConfig.get('workers')
+            observers = []
+            if 'observers' in config:
+                for observer in config['observers']:
+                    observerName = observer["type"]
+                    observerParams = observer["params"]
+                    observers.append(namedAny(observerName)(**observerParams))
 
-        self.buildParameters()
+            records = []
+            if 'accounts' in config:
+                loader = config['accounts']['loader']
+                params = config['accounts']['params']
+                records.extend(namedAny(loader)(**params))
+                output.write("Loaded {0} accounts.\n".format(len(records)))
 
-    def buildParameters(self):
-        self.parameters = PopulationParameters()
-        for client in self.clients:
-            self.parameters.addClient(
+        else:
+            # Python configuration - super easy! Look! It's great!
+                self.webadminPort = serverConfig.get('webadminPort')
+                self.serverStats = serverConfig.get('serverStatsPort')
+                self.observers = serverConfig.get('observers') # Workers shouldn't need this
+                self.workers = serverConfig.get('workers')
+
+                self.server = serverConfig.get('server')
+                self.serializationPath = serverConfig.get('serializationPath')
+                self.arrival = serverConfig.get('arrival')
+                self.records = serverConfig.get('records')
+                self.workerID = serverConfig.get('workerID', 0)
+                self.workerCount = serverConfig.get('workerCount', 1)
+                self.parameters = self.buildParameters(clientConfig)
+
+    def buildParameters(self, clients):
+        parameters = PopulationParameters()
+        for client in clients:
+            parameters.addClient(
                 client["weight"],
-                ClientType(
+                ClientFactory(
                     client["software"],
                     client["params"],
                     client["profiles"]
                 )
             )
+        return parameters
 
     def buildSerializationPath(self):
         if self.serializationPath:
@@ -75,7 +133,54 @@
                     print("Please consult the clientDataSerialization stanza of contrib/performance/loadtest/config.plist")
                     raise
 
-    def get(self, attr):
-        if hasattr(self, attr):
-            return getattr(self, attr)
-        return DEFAULTS.get(attr, None)
+    def serializeForWorker(self, workerID, workerCount):
+        if not self.workers: # If we are workers, don't try to be a manager
+            return {}
+        # print "Trying to serialize for worker #" + str(workerID)
+        # print "My info, btw is " + str(self.__dict__)
+        info = {
+            'webadminPort': '',
+            'serverStats': '',
+            'workers': [],
+            'observers': [],
+            'workerID': workerID,
+            'workerCount': workerCount,
+            # Workers need some information to work correctly
+            'server': self.server,
+            'serializationPath': self.serializationPath,
+            'arrival': self.arrival,
+            'records': self.records,
+            'parameters': self.parameters
+        }
+        return info
+
+    @classmethod
+    def deserializeFromWorker(cls, info):
+        base = cls()
+        base.__dict__.update(info)
+        return base
+
+    # Goodness, how awkward is this code? If we dropped support for plists, we could do away with it
+    @classmethod
+    def _convertParams(cls, params):
+        """
+        Find parameter values which should be more structured than plistlib is
+        capable of constructing and replace them with the more structured form.
+
+        Specifically, find keys that end with C{"Distribution"} and convert
+        them into some kind of distribution object using the associated
+        dictionary of keyword arguments.
+        """
+        for k, v in params.iteritems():
+            if k.endswith('Distribution'): # Goodness how fragile
+                params[k] = cls._convertDistribution(v)
+        return params
+
+
+    @classmethod
+    def _convertDistribution(cls, value):
+        """
+        Construct and return a new distribution object using the type and
+        params specified by C{value}.
+        """
+        return namedAny(value['type'])(**value['params'])
\ No newline at end of file

Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/distributions.py
===================================================================
--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/distributions.py	2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/distributions.py	2015-08-31 20:37:45 UTC (rev 15081)
@@ -26,14 +26,36 @@
 Sampling from this distribution must *not* change the underlying behavior of a distribution
 
 Distributions (all of which implement IDistribution):
-  UniformDiscreteDistribution
-  LogNormalDistribution
-  FixedDistribution
-  NearFutureDistribution
-  NormalDistribution
-  UniformIntegerDistribution
-  WorkDistribution
-  RecurrenceDistribution
+  # Discrete Distributions / Finite Support
+  Bernoulli
+  Binomial
+  Rademacher
+  Fixed
+  UniformDiscrete
+  UniformInteger
+
+  # Discrete Distributions / Infinite Support (> 0)
+  Poisson
+  Geometric
+
+  LogNormal
+
+  Normal
+  UniformReal
+  Triangular
+  Beta
+  ChiSquared
+  Exponential
+  Gamma
+
+  # CalendarServer Specific
+  NearFuture
+  Work
+  Recurrence
+
+# TODO
+Implement simple ones through large ones
+Squeeze / pinch
 """
 from math import log, sqrt
 from time import mktime
@@ -54,9 +76,7 @@
     def sample(): #@NoSelf
         pass
 
-# class Bounded
 
-
 class UniformDiscreteDistribution(object, FancyEqMixin):
     """
 
@@ -65,22 +85,11 @@
 
     compareAttributes = ['_values']
 
-    def __init__(self, values, randomize=True):
+    def __init__(self, values):
         self._values = values
-        self._randomize = randomize
-        self._refill()
 
-
-    def _refill(self):
-        self._remaining = self._values[:]
-        if self._randomize:
-            random.shuffle(self._remaining)
-
-
     def sample(self):
-        if not self._remaining:
-            self._refill()
-        return self._remaining.pop()
+        return random.choice(self._values)
 
 
 
@@ -218,7 +227,20 @@
         return 1 if random.random() <= self._p else 0
 
 
+class RademacherDistribution(object, FancyEqMixin):
+    """
+    Takes value 1 with probability 1/2 and value -1 with probability 1/2
+    """
+    def __init__(self):
+        """
+        """
+        self._d = BernoulliDistribution(proportion=0.5)
 
+    def sample(self):
+        return [-1, 1][self._d.sample()]
+
+
+
 class BinomialDistribution(object, FancyEqMixin):
     compareAttributes = ["_successProbability", "_numTrials"]
 

Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/ical.py
===================================================================
--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/ical.py	2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/ical.py	2015-08-31 20:37:45 UTC (rev 15081)
@@ -249,7 +249,6 @@
         ampPushPort=62311,
     ):
         self._client_id = str(uuid4())
-
         self.reactor = reactor
 
         self.requester = Requester(
@@ -432,6 +431,7 @@
             self.checkCalendarsForEvents, calendarHome)
         return pollCalendarHome.start(self.calendarHomePollInterval, now=False)
 
+    ### TODO this doesn't seem to always work
     @inlineCallbacks
     def updateCalendarHomeFromPush(self, calendarHomeSet):
         """
@@ -538,8 +538,8 @@
                 except KeyError:
                     pass
                 else:
-                    if pushkey:
-                        self.monitor.addPushkey(href, pushkey)
+                    if pushkey and self.monitor:
+                        self.monitor.addPushkey(pushkey, href)
 
             nodes = results[href].getNodeProperties()
             for nodeType in nodes[davxml.resourcetype]:
@@ -888,7 +888,7 @@
 
         # Start monitoring AMP push notifications, if possible
         if self.monitor and self.monitor.isSubscribedTo(calendarHome):
-            self.monitor.begin()
+            yield self.monitor.begin()
             # Run indefinitely.
             yield Deferred()
         else:
@@ -902,6 +902,8 @@
         Called before connections are closed, giving a chance to clean up
         """
         self.serialize()
+        if not self.monitor:
+            return succeed(None)
         return self.monitor.end()
 
 

Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/logger.py
===================================================================
--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/logger.py	2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/logger.py	2015-08-31 20:37:45 UTC (rev 15081)
@@ -107,7 +107,23 @@
             self._printRow(output, formats, self._summarizeData(method, data))
 
 
+class MessageLogger(object):
+    def observe(self, event):
+        if event.get("type") == "log":
+            import random
+            identifier = random.random()
+            print(TerminalColors.WARNING + str(identifier) + '/' + event.get('val') + ':' + event.get('text') + TerminalColors.ENDC)
 
+
+class EverythingLogger(object):
+    def observe(self, event):
+        # if event.get("type") == "response":
+        #     from pprint import pprint
+        #     pprint(event)
+        pass
+
+
+
 class RequestLogger(object):
     format = u"%(user)s request %(code)s%(success)s[%(duration)5.2f s] %(method)8s %(url)s"
     success = u"\N{CHECK MARK}"
@@ -129,7 +145,7 @@
             else:
                 formatArgs['success'] = self.failure
                 start = TerminalColors.FAIL
-            print(start + (self.format % formatArgs).encode('utf-8') + TerminalColors.ENDC)
+            print(start + (self.format % formatArgs).encode('utf-8') + "from Logger w/ id: " + str(id(self)) + TerminalColors.ENDC)
 
 
     def report(self, output):

Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/population.py
===================================================================
--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/population.py	2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/population.py	2015-08-31 20:37:45 UTC (rev 15081)
@@ -43,51 +43,51 @@
 from contrib.performance.loadtest.profiles import Eventer, Inviter, Accepter
 
 
-class ProfileType(object, FancyEqMixin):
-    """
-    @ivar profileType: A L{ProfileBase} subclass, or an L{ICalendarUserProfile}
-        implementation.
+# class ProfileType(object, FancyEqMixin):
+#     """
+#     @ivar profileType: A L{ProfileBase} subclass
+#     @type profileType: C{type}
 
-    @ivar params: A C{dict} which will be passed to C{profileType} as keyword
-        arguments to create a new profile instance.
-    """
-    compareAttributes = ("profileType", "params")
+#     @ivar params: A C{dict} which will be passed to C{profileType} as keyword
+#         arguments to create a new profile instance.
+#     """
+#     compareAttributes = ("profileType", "params")
 
-    def __init__(self, profileType, params):
-        self.profileType = profileType
-        self.params = params
+#     def __init__(self, profileType, params):
+#         self.profileType = profileType
+#         self.params = params
 
 
-    def __call__(self, reactor, simulator, client, number):
-        base = self.profileType(**self.params)
-        base.setUp(reactor, simulator, client, number)
-        return base
+#     def __call__(self, reactor, simulator, client, number):
+#         base = self.profileType(**self.params)
+#         base.setUp(reactor, simulator, client, number)
+#         return base
 
 
-    def __repr__(self):
-        return "ProfileType(%s, params=%s)" % (self.profileType.__name__, self.params)
+#     def __repr__(self):
+#         return "ProfileType(%s, params=%s)" % (self.profileType.__name__, self.params)
 
 
 
-class ClientType(object, FancyEqMixin):
+class ClientFactory(object, FancyEqMixin):
     """
-    @ivar clientType: An L{ICalendarClient} implementation
+    @ivar clientType: An L{BaseAppleClient} subclass
+    @ivar params: A C{dict} which will be passed to C{clientType} as keyword
+        arguements to create a new client
     @ivar profileTypes: A list of L{ProfileType} instances
     """
     compareAttributes = ("clientType", "profileTypes")
 
-    def __init__(self, clientType, clientParams, profileTypes):
+    def __init__(self, clientType, clientParams, profiles):
         self.clientType = clientType
         self.clientParams = clientParams
-        self.profileTypes = profileTypes
+        self.profiles = profiles
 
 
     def new(self, reactor, serverAddress, serializationPath, userRecord, authInfo):
         """
         Create a new instance of this client type.
         """
-        # print(self.clientType)
-        # print(self.clientParams)
         return self.clientType(
             reactor, serverAddress, serializationPath,
             userRecord, authInfo, **self.clientParams
@@ -120,60 +120,67 @@
         self.clients.append((weight, clientType))
 
 
-    def clientTypes(self):
-        """
-        Return a list of two-tuples giving the weights and types of
-        clients in the population.
-        """
-        return self.clients
+    def clientGenerator(self):
+        while True:
+            for (weight, clientFactory) in self.clients:
+                for _ignore_i in xrange(weight):
+                    yield clientFactory
 
 
+    # def clientTypes(self):
+    #     """
+    #     Return a list of two-tuples giving the weights and types of
+    #     clients in the population.
+    #     """
+    #     return self.clients
 
-class Populator(object):
-    """
-    @ivar userPattern: A C{str} giving a formatting pattern to use to
-        construct usernames.  The string will be interpolated with a
-        single integer, the incrementing counter of how many users
-        have thus far been "used".
 
-    @ivar passwordPattern: Similar to C{userPattern}, but for
-        passwords.
-    """
-    def __init__(self, random):
-        self._random = random
 
 
-    def _cycle(self, elements):
-        while True:
-            for (weight, value) in elements:
-                for _ignore_i in range(weight):
-                    yield value
 
+# class Populator(object):
+#     """
+#     """
+#     def __init__(self):
+#         self._random = random
 
-    def populate(self, parameters):
-        """
-        Generate individuals such as might be randomly selected from a
-        population with the given parameters.
 
-        @type parameters: L{PopulationParameters}
-        @rtype: generator of L{ClientType} instances
-        """
-        for (clientType,) in izip(self._cycle(parameters.clientTypes())):
-            yield clientType
+#     def _cycle(self, elements):
+#         while True:
+#             for (weight, value) in elements:
+#                 for _ignore_i in range(weight):
+#                     yield value
 
 
+#     def populate(self, parameters):
+#         """
+#         Generate individuals such as might be randomly selected from a
+#         population with the given parameters.
 
+#         @type parameters: L{PopulationParameters}
+#         @rtype: generator of L{ClientType} instances
+#         """
+#         for (clientType,) in izip(self._cycle(parameters.clientTypes())):
+#             yield clientType
+
+
+
 class CalendarClientSimulator(object):
-    def __init__(self, records, populator, parameters, reactor, server,
+    def __init__(self, records, parameters, reactor, server,
                  serializationPath, workerIndex=0, workerCount=1):
+        import random
+        # i = random.randint(0, 1000)
+        # with open('log%d.txt'.format(i), 'a') as f:
+        #     f.write('wtf')
+        val = random.random()
+        msg(type="log", text="In create client sim", val=str(val))
         from pprint import pprint
         pprint(records)
         self._records = records
-        self.populator = populator
         self.reactor = reactor
         self.server = server
         self.serializationPath = serializationPath
-        self._pop = self.populator.populate(parameters)
+        self._populator = parameters.clientGenerator()
         self._user = 0
         self._stopped = False
         self.workerIndex = workerIndex
@@ -228,63 +235,74 @@
 
 
     def add(self, numClients, clientsPerUser):
-        for _ignore_n in range(numClients):
+        # for _ignore_n in range(numClients):
+        #     number = self._nextUserNumber()
+
+        #     for _ignore_peruser in range(clientsPerUser):
+        #         clientType = self._populator.next()
+        #         if (number % self.workerCount) != self.workerIndex:
+        #             # If we're in a distributed work scenario and we are worker N,
+        #             # we have to skip all but every Nth request (since every node
+        #             # runs the same arrival policy).
+        #             continue
+
+        #         _ignore_user, auth = self._createUser(number)
+
+        #         reactor = loggedReactor(self.reactor)
+        #         client = clientType.new(
+        #             reactor,
+        #             self.server,
+        #             self.serializationPath,
+        #             self.getUserRecord(number),
+        #             auth,
+        #         )
+        #         self.clients.append(client)
+        #         d = client.run()
+        #         d.addErrback(self._clientFailure, reactor)
+
+        #         for profileType in clientType.profileTypes:
+        #             print(profileType)
+        #             profile = profileType(reactor, self, client, number)
+        #             if profile.enabled:
+        #                 d = profile.initialize()
+        #                 def _run(result):
+        #                     d2 = profile.run()
+        #                     d2.addErrback(self._profileFailure, profileType, reactor)
+        #                     return d2
+        #                 d.addCallback(_run)
+
+        for i in range(numClients):
             number = self._nextUserNumber()
-
-            for _ignore_peruser in range(clientsPerUser):
-                clientType = self._pop.next()
+            # What user are we representing?
+            for j in range(clientsPerUser):
                 if (number % self.workerCount) != self.workerIndex:
                     # If we're in a distributed work scenario and we are worker N,
                     # we have to skip all but every Nth request (since every node
                     # runs the same arrival policy).
                     continue
+                clientFactory = self._populator.next()
 
                 _ignore_user, auth = self._createUser(number)
+                reactor = loggedReactor(self.reactor)
 
-                reactor = loggedReactor(self.reactor)
-                client = clientType.new(
-                    reactor,
+                client = clientFactory.new(
+                    self.reactor,
                     self.server,
                     self.serializationPath,
                     self.getUserRecord(number),
-                    auth,
+                    auth
                 )
+                import random
                 self.clients.append(client)
-                d = client.run()
-                d.addErrback(self._clientFailure, reactor)
+                client.run().addErrback(self._clientFailure, reactor)
+                for profileTemplate in clientFactory.profiles:
+                    print("Templated interval is:" + str(profileTemplate._interval))
+                    profile = profileTemplate.duplicate()
+                    print("I see the interval as:" + str(profile._interval))
+                    profile.setUp(self.reactor, self, client, number)
+                    profile.run().addErrback(self._profileFailure, reactor)
 
-                for profileType in clientType.profileTypes:
-                    print(profileType)
-                    profile = profileType(reactor, self, client, number)
-                    if profile.enabled:
-                        d = profile.initialize()
-                        def _run(result):
-                            d2 = profile.run()
-                            d2.addErrback(self._profileFailure, profileType, reactor)
-                            return d2
-                        d.addCallback(_run)
 
-        # XXX this status message is prone to be slightly inaccurate, but isn't
-        # really used by much anyway.
-        msg(type="status", clientCount=self._user - 1)
-
-        # for i in range(numClients):
-        #     for j in range(clientsPerUser):
-        #         client = self._pop.next()
-        #         # Reactor magic goes here
-        #         client.setUp(
-        #             self.reactor,
-        #             self.server,
-        #             self.serializationPath,
-        #             self.getUserRecord(number),
-        #             auth
-        #         )
-        #         for profile in client.profiles:
-        #             profile.setUp(self.reactor, self, client, number)
-
-
-
-
     def _dumpLogs(self, loggingReactor, reason):
         path = FilePath(mkdtemp())
         logstate = loggingReactor.getLogFiles()
@@ -297,7 +315,7 @@
         return path
 
 
-    def _profileFailure(self, reason, profileType, reactor):
+    def _profileFailure(self, reason, reactor):
         if not self._stopped:
             where = self._dumpLogs(reactor, reason)
             err(reason, "Profile stopped with error; recent traffic in %r" % (
@@ -321,17 +339,16 @@
 
 
 class SmoothRampUp(object):
-    def __init__(self, reactor, groups, groupSize, interval, clientsPerUser):
-        self.reactor = reactor
+    def __init__(self, groups, groupSize, interval, clientsPerUser):
         self.groups = groups
         self.groupSize = groupSize
         self.interval = interval
         self.clientsPerUser = clientsPerUser
 
 
-    def run(self, simulator):
+    def run(self, reactor, simulator):
         for i in range(self.groups):
-            self.reactor.callLater(
+            reactor.callLater(
                 self.interval * i, simulator.add, self.groupSize, self.clientsPerUser)
 
 

Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/profiles.py
===================================================================
--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/profiles.py	2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/profiles.py	2015-08-31 20:37:45 UTC (rev 15081)
@@ -23,6 +23,7 @@
 
 import random
 from uuid import uuid4
+from numbers import Number
 
 from caldavclientlibrary.protocol.caldav.definitions import caldavxml
 
@@ -37,7 +38,7 @@
 
 from contrib.performance.loadtest.distributions import (
     NearFutureDistribution, NormalDistribution, UniformDiscreteDistribution, BernoulliDistribution,
-    LogNormalDistribution, RecurrenceDistribution
+    LogNormalDistribution, RecurrenceDistribution, FixedDistribution
 )
 from contrib.performance.loadtest.ical import IncorrectResponseCode
 from contrib.performance.loadtest.resources import Calendar, Event
@@ -47,6 +48,23 @@
 from pycalendar.duration import Duration
 from pycalendar.value import Value
 
+def _loopWithDistribution(reactor, distribution, function):
+    result = Deferred()
+    print(distribution)
+
+    def repeat(ignored):
+        reactor.callLater(distribution.sample(), iterate)
+
+    def iterate():
+        d = function()
+        if d is not None:
+            d.addCallbacks(repeat, result.errback)
+        else:
+            repeat(None)
+
+    repeat(None)
+    return result
+
 class ProfileBase(object):
     """
     Base class which provides some conveniences for profile
@@ -55,11 +73,19 @@
     random = random
 
     def __init__(self, enabled, interval, **params):
+        print("Creating new profile: %s" % (self.__class__.__name__,))
         self.enabled = enabled
+        if isinstance(interval, Number):
+            interval = FixedDistribution(interval)
         self._interval = interval
-        self.setParameters(**params)
+        print "**" + str(self._interval)
+        self._params = params
+        self.setDistributions(**params)
         self._initialized = False
 
+    def duplicate(self):
+        return type(self)(enabled=self.enabled, interval=self._interval, **self._params)
+
     def setUp(self, reactor, simulator, client, record):
         self._reactor = reactor
         self._sim = simulator
@@ -67,49 +93,19 @@
         self._record = record
         self._initialized = True
 
-    def setParameters(self):
+    def setDistributions(self):
         pass
 
+    def _wrapper(self):
+        if not self.enabled:
+            return succeed(None)
+        if not self._client.started:
+            return succeed(None)
+        return self.action()
+
     def run(self):
-        # def action(self):
-        #     if self.enabled:
-        #         return self.action()
+        return _loopWithDistribution(self._reactor, self._interval, self._wrapper)
 
-        print("Hello from run")
-        _call = LoopingCall(self.action)
-        _call.clock = self._reactor
-        return _call.start(self._interval)
-
-        # OR
-
-    #     return self._loopWithDistribution(
-    #         self._reactor, self._sendInvitationDistribution, self._invite)
-
-    # def _loopWithDistribution(reactor, distribution, function):
-    #     result = Deferred()
-
-    #     def repeat(ignored):
-    #         reactor.callLater(distribution.sample(), iterate)
-
-    #     def iterate():
-    #         d = function()
-    #         if d is not None:
-    #             d.addCallbacks(repeat, result.errback)
-    #         else:
-    #             repeat(None)
-
-    #     repeat(None)
-    #     return result
-
-    def initialize(self):
-        """
-        Called before the profile runs for real. Can be used to initialize client state.
-
-        @return: a L{Deferred} that fires when initialization is done
-        """
-        return succeed(None)
-
-
     def _calendarsOfType(self, calendarType, componentType):
         return [
             cal
@@ -127,12 +123,12 @@
         return attendee.parameterValue('EMAIL') == self._client.email[len('mailto:'):]
 
 
-    def _getRandomCalendar(self):
+    def _getRandomCalendarOfType(self, component_type):
         """
         Return a random L{Calendar} object from the current user
         or C{None} if there are no calendars to work with
         """
-        calendars = self._calendarsOfType(caldavxml.calendar, "VEVENT")
+        calendars = self._calendarsOfType(caldavxml.calendar, component_type)
         if not calendars: # Oh no! There are no calendars to play with
             return None
         # Choose a random calendar
@@ -140,12 +136,12 @@
         return calendar
 
 
-    def _getRandomEvent(self):
+    def _getRandomEventOfType(self, component_type):
         """
         Return a random L{Event} object from the current user
         or C{None} if there are no events to work with
         """
-        calendars = self._calendarsOfType(caldavxml.calendar, "VEVENT")
+        calendars = self._calendarsOfType(caldavxml.calendar, component_type)
         while calendars:
             calendar = self.random.choice(calendars)
             calendars.remove(calendar)
@@ -232,50 +228,42 @@
     """
     pass
 
-    """ Event-Interaction Profiles
-Event Creation - Eventer
-Event Changing - EventUpdaterBase
-                    TitlerMixin
-                    RelocaterMixin
-                    ReschedulerMixin
-                    RepeaterMixin
-                    AlerterMixin
-                    InviterMixin
-                    NoterMixin
-                    InviterMixin
-                    LinkerMixin
-                    AttacherMixin
-Event Deletion - EventerDeleter
-"""
 
 #####################
-# Eventer Behaviors #
+# Eventer Hierarchy #
+# ----------------- #
+# EventBase         #
+#   Eventer         #
+#   EventUpdaterBase#
+#     Titler        #
+#     Noter         #
+#     Linker        #
+#     Relocater     #
+#     Repeater      #
+#     Rescheduler   #
+#     ~Alerter~     #
+#     Attacher      #
+#     InviterBase   #
+#       Inviter     #
+#       Relocater   #
+#   EventDeleter    #
 #####################
 
-class EventerBase(ProfileBase):
+class EventBase(ProfileBase):
     """
     Base profile for a calendar user who interacts with events
     """
-    def setParameters(
-        self,
-        enabled=True,
-        interval=25,
-        **params
-    ):
-        self.enabled = enabled
-        self._interval = interval
-        self.setDistributions(**params)
+    def _getRandomCalendar(self):
+        return self._getRandomCalendarOfType('VEVENT')
 
+    def _getRandomEvent(self):
+        return self._getRandomEventOfType('VEVENT')
 
 
-class Eventer(EventerBase):
+class Eventer(EventBase):
     """
     A Calendar user who creates new events.
     """
-    def initialize(self):
-        self.action = self._addEvent
-        return succeed(None)
-
     def setDistributions(
         self,
         eventStartDistribution=NearFutureDistribution(),
@@ -285,30 +273,25 @@
             120 * 60
         ])
     ):
-        self._eventStartDistribution = eventStartDistribution
-        self._eventDurationDistribution = eventDurationDistribution
+        """
+        @param eventStartDistribution: Generates datetimes at which an event starts
+        @param eventDurationDistribution: Generates length of event (in seconds)
+        """
+        self._eventStart = eventStartDistribution
+        self._eventDuration = eventDurationDistribution
 
     def _addEvent(self):
-        print "Hello a bit"
-
-        if not self._client.started:
-            return succeed(None)
-
-
-
+        # Choose a random calendar on which to add an event
         calendar = self._getRandomCalendar()
         if not calendar:
             return succeed(None)
 
-        print "Made it"
-
-        # Copy the template event and fill in some of its fields
-        # to make a new event to create on the calendar.
+        # Form a new event by modifying fields of the template event
         vcalendar = eventTemplate.duplicate()
         vevent = vcalendar.mainComponent()
         uid = str(uuid4())
-        dtstart = self._eventStartDistribution.sample()
-        dtend = dtstart + Duration(seconds=self._eventDurationDistribution.sample())
+        dtstart = self._eventStart.sample()
+        dtend = dtstart + Duration(seconds=self._eventDuration.sample())
 
         vevent.replaceProperty(Property("UID", uid))
         vevent.replaceProperty(Property("CREATED", DateTime.getNowUTC()))
@@ -319,17 +302,18 @@
         href = '%s%s.ics' % (calendar.url, uid)
         event = Event(self._client.serializeLocation(), href, None, component=vcalendar)
         d = self._client.addEvent(href, event)
-        return self._newOperation("create", d)
+        return self._newOperation("create{event}", d)
 
+    action = _addEvent
+
 # Could have better handling for not changing events once they're modified
 # esp re: repeating
-class EventUpdaterBase(EventerBase):
+class EventUpdaterBase(EventBase):
     """Superclass of all event mixins.
     Accepts two parameters
     enabled: bool on or off
     interval: distibution that generates integers representing delays
     """
-    # COMPONENT_TYPE = None
     def action(self):
         event = self._getRandomEvent()
         if not event:
@@ -346,7 +330,7 @@
 
         return self._newOperation(label, d)
 
-    def modifyEvent(self):
+    def modifyEvent(self, href, vevent):
         """Overriden by subclasses"""
         pass
 
@@ -450,19 +434,21 @@
         return "reschedule{event}"
 
 # class Alerter(EventUpdaterBase):
+# component.replaceProperty(Property("ACKNOWLEDGED", DateTime.getNowUTC()))
 #     pass
 
 class Attacher(EventUpdaterBase):
     def setDistributions(
         self,
+        filesizeDistribution=NormalDistribution(24, 3),
+        numAttachmentsDistribution=LogNormalDistribution(2, 1),
+        attachLikelihoodDistribution=BernoulliDistribution(0.9),
+
     ):
-    # filesizeDistribution=NormalDistribution(24, 3),
-    # numAttachmentsDistribution=LogNormalDistribution(2, 1),
-    # attachLikelihoodDistribution=BernoulliDistribution(0.9)
-        # self._filesize = filesizeDistribution
+        self._filesize = filesizeDistribution
         # self._numAttachments = numAttachmentsDistribution
         # self._attachLikelihood = attachLikelihoodDistribution
-        pass
+        # pass
 
     def modifyEvent(self, href, vevent):
         d = self._client.postAttachment(href, 'x' * 1024)
@@ -487,405 +473,52 @@
     def unattachFile(self):
         pass
 
-class InviterBase(EventerBase):
-    """
-    Base profile for a calendar user that invites and deinvites other principals to events
-    """
-    def setParameters(
+class InviterBase(EventUpdaterBase):
+    def setDistributions(
         self,
-        enabled=True,
+        numInvitees=NormalDistribution(7, 2),
         sendInvitationDistribution=NormalDistribution(600, 60),
-        inviteeDistribution=UniformDiscreteDistribution(range(-10, 11)),
-        **params
+        numInviteesDistribution=UniformDiscreteDistribution(range(-10, 11))
     ):
         self.enabled = enabled
         self._sendInvitationDistribution = sendInvitationDistribution
-        self._inviteeDistribution = inviteeDistribution
-        if len(params) > 0:
-            pass
+        self._numInvitees = inviteeDistribution
 
-    def getAttendees():
+    def _findUninvitedRecord(self, vevent):
         pass
 
-    # def _invitePrincipal(self, ...):
+    def _addAttendee(self, some_id):
+        attendeeProp = self._buildAttendee()
+        self._client.attendeeAutocomplete
 
-    # def _uninvitePrincipal(self, ...):
-
-
-
-
-    def _loopWithDistribution(reactor, distribution, function):
-        result = Deferred()
-
-        def repeat(ignored):
-            reactor.callLater(distribution.sample(), iterate)
-
-        def iterate():
-            d = function()
-            if d is not None:
-                d.addCallbacks(repeat, result.errback)
-            else:
-                repeat(None)
-
-        repeat(None)
-        return result
-
     # def _didSelfOrganize(self, vevent):
 
+    # TODO handle alternate roles
+    def _buildAttendee(self, commonname, cuaddr, isIndividual=True, isRequired=False):
+        return Property(
+            name=u'ATTENDEE',
+            value=cuaddr.encode("utf-8"),
+            params={
+                'CN': commonname,
+                'CUTYPE': 'INDIVIDUAL' if isIndividual else 'ROOM',
+                'PARTSTAT': 'NEEDS-ACTION',
+                'ROLE': 'REQ-PARTICIPANT',
+                'RSVP': 'TRUE',
+            },
+        )
 
-    # def _buildIndividualAttendee(self, commonName, record, ):
+    def _getAttendees(self, vevent):
+        return vevent.properties('ATTENDEE')
 
-    #     # ATTENDEE;CN="Super User";CUTYPE=INDIVIDUAL;EMAIL="admin at example.com":mailto:admin at example.com
-    #     # ATTENDEE;CN="User 04";CUTYPE=INDIVIDUAL;EMAIL="user04 at example.com":mailto:user04 at example.com
-
-    #     role = ['REQ-PARTICIPANT', '']
-    #     attendee = Property(
-    #         name=u'ATTENDEE',
-    #         value=cuaddr.encode("utf-8"),
-    #         params={
-    #             'CN': commonName,
-    #             'CUTYPE': 'INDIVIDUAL',
-    #             'PARTSTAT': 'NEEDS-ACTION',
-    #             'ROLE': 'REQ-PARTICIPANT',
-    #             'RSVP': 'TRUE',
-    #         },
-    #     )
-    #     return attendee
-
-    # def _buildLocationAttendee(self, cn, cuaddr):
-    #     """
-    #     Example Usage: profile._buildLocationAttendee("Location 01", "urn:uuid:...")
-    #     """
-    #     role = ['REQ-PARTICIPANT', '']
-    #     attendee = Property(
-    #         name=u'ATTENDEE',
-    #         value=cuaddr.encode("utf-8"),
-    #         params={
-    #             'CN': commonName,
-    #             'CUTYPE': 'ROOM',
-    #             'PARTSTAT': 'NEEDS-ACTION',
-    #             'ROLE': 'REQ-PARTICIPANT',
-    #             'RSVP': 'TRUE',
-    #         },
-    #     )
-    #     return attendee
-
     def _invite():
         raise NotImplementedError
 
     def _addAttendee():
         raise NotImplementedError
 
-# TODO - invite groups
-# class Inviter(EventUpdaterBase): pass
+class Inviter(InviterBase):
 
-class Relocater(EventUpdaterBase):
-    def setParameters(
-        self,
-    ):
-        pass
-
-class AppleEventer(EventerBase):
-    """
-    Calendar user who makes events in the form of Apple logo
-    """
     def initialize(self):
-        self.action = self._addEvent
-        return succeed(None)
-
-    def setDistributions(
-        self
-    ):
-        pass
-
-    def _addEvent(self):
-        if not self._client.started:
-            return succeed(None)
-
-        calendar = self._getRandomCalendar()
-        if not calendar:
-            return succeed(None)
-
-        # Copy the template event and fill in some of its fields
-        # to make a new event to create on the calendar.
-        vcalendar = eventTemplate.duplicate()
-        vevent = vcalendar.mainComponent()
-        uid = str(uuid4())
-        dtstart = self._eventStartDistribution.sample()
-        dtend = dtstart + Duration(seconds=self._eventDurationDistribution.sample())
-
-        vevent.replaceProperty(Property("UID", uid))
-        vevent.replaceProperty(Property("CREATED", DateTime.getNowUTC()))
-        vevent.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC()))
-        vevent.replaceProperty(Property("DTSTART", dtstart))
-        vevent.replaceProperty(Property("DTEND", dtend))
-
-        href = '%s%s.ics' % (calendar.url, uid)
-        event = Event(self._client.serializeLocation(), href, None, component=vcalendar)
-        d = self._client.addEvent(href, event)
-        return self._newOperation("create", d)
-
-class HappyEventer(EventerBase):
-    """
-    Calendar user who makes events in the form of Apple logo
-    """
-    def initialize(self):
-        self.action = self._addEvent
-        return succeed(None)
-
-    def setDistributions(
-        self
-    ):
-        self._eventStart = UniformDiscreteDistribution([
-            # August 3
-            DateTime(year=2015, month=8, day=3, hours=15, minutes=0, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=3, hours=15, minutes=30, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=3, hours=16, minutes=0, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=3, hours=16, minutes=30, seconds=0, tzid=None, utcoffset=None),
-
-            # August 4
-            DateTime(year=2015, month=8, day=4, hours=10, minutes=0, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=4, hours=10, minutes=30, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=4, hours=11, minutes=0, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=4, hours=11, minutes=30, seconds=0, tzid=None, utcoffset=None),
-
-            DateTime(year=2015, month=8, day=4, hours=16, minutes=0, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=4, hours=16, minutes=30, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=4, hours=17, minutes=0, seconds=0, tzid=None, utcoffset=None),
-
-            # August 5
-            DateTime(year=2015, month=8, day=5, hours=13, minutes=0, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=5, hours=13, minutes=30, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=5, hours=17, minutes=0, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=5, hours=17, minutes=30, seconds=0, tzid=None, utcoffset=None),
-
-            # August 6
-            DateTime(year=2015, month=8, day=6, hours=10, minutes=0, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=6, hours=10, minutes=30, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=6, hours=11, minutes=0, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=6, hours=11, minutes=30, seconds=0, tzid=None, utcoffset=None),
-
-            DateTime(year=2015, month=8, day=6, hours=16, minutes=0, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=6, hours=16, minutes=30, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=6, hours=17, minutes=0, seconds=0, tzid=None, utcoffset=None),
-
-            # August 7
-            DateTime(year=2015, month=8, day=7, hours=15, minutes=0, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=7, hours=15, minutes=30, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=7, hours=16, minutes=0, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=7, hours=16, minutes=30, seconds=0, tzid=None, utcoffset=None),
-        ])
-        self._eventDuration = UniformDiscreteDistribution([
-            30 * 60,
-            60 * 60
-        ])
-
-    def _addEvent(self):
-        if not self._client.started:
-            return succeed(None)
-
-        calendar = self._getRandomCalendar()
-        if not calendar:
-            return succeed(None)
-
-        # Copy the template event and fill in some of its fields
-        # to make a new event to create on the calendar.
-        vcalendar = eventTemplate.duplicate()
-        vevent = vcalendar.mainComponent()
-        uid = str(uuid4())
-        dtstart = self._eventStart.sample()
-        dtend = dtstart + Duration(seconds=self._eventDuration.sample())
-
-        vevent.replaceProperty(Property("UID", uid))
-        vevent.replaceProperty(Property("CREATED", DateTime.getNowUTC()))
-        vevent.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC()))
-
-        vevent.replaceProperty(Property("DTSTART", dtstart))
-        vevent.replaceProperty(Property("DTEND", dtend))
-
-        href = '%s%s.ics' % (calendar.url, uid)
-        event = Event(self._client.serializeLocation(), href, None, component=vcalendar)
-        d = self._client.addEvent(href, event)
-        return self._newOperation("create", d)
-
-
-
-# class EventDeleter(ProfileBase):
-
-
-
-""" TEST """
-# class Intern(object):
-#     def __init__(self):
-#         self.behaviors = [
-#             Eventer(asdfjadsf),
-#             Attacher(asjadsfjasdf),
-#             Inviter(enabled=True, **params)
-#         ]
-
-#     def run(self):
-#         deferreds = []
-#         for behavior in self.behaviors:
-#             deferreds.append(behavior.run())
-#         return DeferredList(deferreds)
-
-
-
-####################
-# Class Hierarchy
-# ---------------
-# TaskBase
-#   Tasker
-#   TaskUpdaterBase
-#     Titler
-#     Alerter
-#     Noter
-#     Prioritizer
-#     Completer
-#   TaskDeleter
-####################
-class TaskBase(ProfileBase):
-    """
-    Base profile for a calendar user who interacts with tasks
-    """
-    def setParameters(self, enabled=True, interval=25, **params):
-        self.enabled = enabled
-        self._interval = interval
-
-class Tasker(TaskBase):
-    """
-    A Calendar user who creates new tasks.
-    """
-    def initialize(self):
-        self.action = self._addTask
-        return succeed(None)
-
-    def _addTask(self, title="Simple Task"):
-        if not self._client.started:
-            return succeed(None)
-
-        calendars = self._calendarsOfType(caldavxml.calendar, "VTODO")
-
-        while calendars:
-            calendar = self.random.choice(calendars)
-            calendars.remove(calendar)
-
-            # Copy the template task and fill in some of its fields
-            # to make a new task to create on the calendar.
-            vcalendar = taskTemplate.duplicate()
-            vtodo = vcalendar.mainComponent()
-            uid = str(uuid4())
-            vtodo.replaceProperty(Property("UID", uid))
-            vtodo.replaceProperty(Property("CREATED", DateTime.getNowUTC()))
-            vtodo.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC()))
-
-            href = '%s%s.ics' % (calendar.url, uid)
-            d = self._client.addEvent(href, vcalendar)
-            return self._newOperation("create", d)
-
-
-class TaskUpdaterBase(TaskBase):
-    def action(self):
-        task = self._getRandomTask()
-        if not task:
-            return succeed(None)
-        component = task.component
-        vtodo = component.mainComponent()
-
-        label = self.modifyEvent(task.url, vtodo)
-        vtodo.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC()))
-
-        task.component = component
-        d = self._client.updateEvent(task)
-        return self._newOperation(label, d)
-
-    def modifyEvent(self):
-        """Overriden by subclasses"""
-        pass
-
-
-
-
-class TaskUpdater(TaskBase):
-    """
-    A Calendar user who creates and updates complex tasks with:
-      Mark as completed/not completed
-      Change name
-      Change priority
-      Change notes
-      Sets due dates
-    """
-
-    def setParameters(
-        self,
-        taskDueDistribution=NearFutureDistribution(),
-    ):
-        pass
-
-
-    def _addTask(self):
-        if not self._client.started:
-            return succeed(None)
-
-        calendars = self._calendarsOfType(caldavxml.calendar, "VTODO")
-
-        while calendars:
-            calendar = self.random.choice(calendars)
-            calendars.remove(calendar)
-
-            # Copy the template task and fill in some of its fields
-            # to make a new task to create on the calendar.
-            vcalendar = taskTemplate.duplicate()
-            vtodo = vcalendar.mainComponent()
-            uid = str(uuid4())
-            due = self._taskStartDistribution.sample()
-            vtodo.replaceProperty(Property("CREATED", DateTime.getNowUTC()))
-            vtodo.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC()))
-            vtodo.replaceProperty(Property("UID", uid))
-            
-            # vtodo.replaceProperty(Property("SUMMARY", title))
-
-            href = '%s%s.ics' % (calendar.url, uid)
-            d = self._client.addEvent(href, vcalendar)
-            return self._newOperation("create", d)
-
-
-
-# class TaskTitlerMixin = TitlerMixin
-# class TaskAlerterMixin = AlerterMixin
-# self._taskStartDistribution = taskDueDistribution
-# vtodo.replaceProperty(Property("DUE", due))
-# class TaskNoterMixin = NoterMixin
-
-class Prioritizer(TaskBase):
-    PRIORITY_NONE = 0
-    PRIORITY_HIGH = 1
-    PRIORITY_MEDIUM = 5
-    PRIORITY_LOW = 9
-
-    def _setPriority(self, priority, vtodo):
-        vtodo.replaceProperty(Property("PRIORITY", priority))
-
-class Completer(TaskBase):
-    def _markTaskComplete(vtodo):
-        """ task is a Component representing a VTODO """
-        vtodo.replaceProperty(Property("COMPLETED", DateTime.getNowUTC()))
-        vtodo.replaceProperty(Property("PERCENT-COMPLETE", 100))
-        vtodo.replaceProperty(Property("STATUS", "COMPLETED"))
-
-
-    def _markTaskIncomplete(vtodo):
-        """ mark a VTODO as incomplete """
-        vtodo.removeProperty("COMPLETED")
-        vtodo.removeProperty("PERCENT-COMPLETE")
-        vtodo.replaceProperty(Property("STATUS", "NEEDS-ACTION"))
-
-# class TaskDeleter(ProfileBase): pass
-
-
-class Inviter(ProfileBase):
-
-    def initialize(self):
         self.action = self.test
         return succeed(None)
 
@@ -1014,135 +647,198 @@
         # Oops, either no events or no calendars to play with.
         return succeed(None)
 
+class Relocater(InviterBase):
+    def setDistributions(
+        self,
+    ):
+        pass
 
+class EventDeleter(EventBase):
+    """
+    A calendar user who deletes events at random
+    """
+    def _deleteEvent(self):
+        event = self._getRandomEvent()
+        if event is None:
+            return succeed(None)
+        d = self._client.deleteEvent(event.url)
+        return self._newOperation("delete{event}", d)
 
-class RealisticInviter(ProfileBase):
+    action = _deleteEvent
+
+
+
+
+""" TEST """
+# class Intern(object):
+#     def __init__(self):
+#         self.behaviors = [
+#             Eventer(asdfjadsf),
+#             Attacher(asjadsfjasdf),
+#             Inviter(enabled=True, **params)
+#         ]
+
+#     def run(self):
+#         deferreds = []
+#         for behavior in self.behaviors:
+#             deferreds.append(behavior.run())
+#         return DeferredList(deferreds)
+
+
+
+####################
+# Tasker Hierarchy #
+# ---------------- #
+# TaskBase         #
+#   Tasker         #
+#   TaskDeleter    #
+#   TaskUpdaterBase#
+#     Titler       #
+#     Noter        #
+#     Prioritizer  #
+#     Completer    #
+#     Alerter      #
+####################
+
+
+class TaskBase(ProfileBase):
     """
-    A Calendar user who invites other users to new events.
+    Base profile for a calendar user who interacts with tasks
     """
-    def setParameters(
-        self,
-        enabled=True,
-        sendInvitationDistribution=NormalDistribution(600, 60),
-        inviteeDistribution=UniformDiscreteDistribution(range(-10, 11)),
-        inviteeClumping=True,
-        inviteeCountDistribution=LogNormalDistribution(1.2, 1.2),
-        eventStartDistribution=NearFutureDistribution(),
-        eventDurationDistribution=UniformDiscreteDistribution([
-            15 * 60, 30 * 60,
-            45 * 60, 60 * 60,
-            120 * 60
-        ]),
-        recurrenceDistribution=RecurrenceDistribution(False),
-    ):
-        self.enabled = enabled
-        self._sendInvitationDistribution = sendInvitationDistribution
-        self._inviteeDistribution = inviteeDistribution
-        self._inviteeClumping = inviteeClumping
-        self._inviteeCountDistribution = inviteeCountDistribution
-        self._eventStartDistribution = eventStartDistribution
-        self._eventDurationDistribution = eventDurationDistribution
-        self._recurrenceDistribution = recurrenceDistribution
+    def _getRandomCalendar(self):
+        return self._getRandomCalendarOfType('VTODO')
 
+    def _getRandomEvent(self):
+        return self._getRandomEventOfType('VTODO')
 
-    def _addAttendee(self, event, attendees):
-        """
-        Create a new attendee to add to the list of attendees for the
-        given event.
-        """
-        selfRecord = self._sim.getUserRecord(self._number)
-        invitees = set([u'mailto:%s' % (selfRecord.email,)])
-        for att in attendees:
-            invitees.add(att.value())
 
-        for _ignore_i in range(10):
+class Tasker(TaskBase):
+    """
+    A Calendar user who creates new tasks.
+    """
+    def _addTask(self):
+        calendar = self._getRandomCalendar()
+        if not calendar:
+            return succeed(None)
 
-            sample = self._inviteeDistribution.sample()
-            if self._inviteeClumping:
-                sample = self._number + sample
-            invitee = max(0, sample)
+        # Form a new event by modifying fields of the template event
+        vcalendar = taskTemplate.duplicate()
+        vtodo = vcalendar.mainComponent()
+        uid = str(uuid4())
 
-            try:
-                record = self._sim.getUserRecord(invitee)
-            except IndexError:
-                continue
-            cuaddr = u'mailto:%s' % (record.email,)
-            if cuaddr not in invitees:
-                break
-        else:
-            raise CannotAddAttendee("Can't find uninvited user to invite.")
+        vtodo.replaceProperty(Property("UID", uid))
+        vtodo.replaceProperty(Property("CREATED", DateTime.getNowUTC()))
+        vtodo.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC()))
 
-        attendee = Property(
-            name=u'ATTENDEE',
-            value=cuaddr.encode("utf-8"),
-            params={
-                'CN': record.commonName,
-                'CUTYPE': 'INDIVIDUAL',
-                'PARTSTAT': 'NEEDS-ACTION',
-                'ROLE': 'REQ-PARTICIPANT',
-                'RSVP': 'TRUE',
-            },
-        )
+        href = '%s%s.ics' % (calendar.url, uid)
+        event = Event(self._client.serializeLocation(), href, None, component=vcalendar)
+        d = self._client.addEvent(href, event)
+        return self._newOperation("create{task}", d)
 
-        event.addProperty(attendee)
-        attendees.append(attendee)
+    action = _addTask
 
+class TaskDeleter(TaskBase):
+    def _deleteTask(self):
+        event = self._getRandomEvent()
+        if event is None:
+            return succeed(None)
 
-    def _invite(self):
-        """
-        Try to add a new event, or perhaps remove an
-        existing attendee from an event.
+        d = self._client.deleteEvent(event.url)
+        return self._newOperation("delete{task}", d)
 
-        @return: C{None} if there are no events to play with,
-            otherwise a L{Deferred} which fires when the attendee
-            change has been made.
-        """
+    action = _deleteTask
 
-        if not self._client.started:
+
+class TaskUpdaterBase(TaskBase):
+    def action(self):
+        task = self._getRandomEvent()
+        if not task:
             return succeed(None)
+        component = task.component
+        vtodo = component.mainComponent()
 
-        # Find calendars which are eligible for invites
-        calendars = self._calendarsOfType(caldavxml.calendar, "VEVENT")
+        label = self.modifyEvent(task.url, vtodo)
+        vtodo.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC()))
 
-        while calendars:
-            # Pick one at random from which to try to create an event
-            # to modify.
-            calendar = self.random.choice(calendars)
-            calendars.remove(calendar)
+        task.component = component
+        d = self._client.updateEvent(task, method_label="update{task}")
+        return self._newOperation(label, d)
 
-            # Copy the template event and fill in some of its fields
-            # to make a new event to create on the calendar.
-            vcalendar = eventTemplate.duplicate()
-            vevent = vcalendar.mainComponent()
-            uid = str(uuid4())
-            dtstart = self._eventStartDistribution.sample()
-            dtend = dtstart + Duration(seconds=self._eventDurationDistribution.sample())
-            vevent.replaceProperty(Property("CREATED", DateTime.getNowUTC()))
-            vevent.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC()))
-            vevent.replaceProperty(Property("DTSTART", dtstart))
-            vevent.replaceProperty(Property("DTEND", dtend))
-            vevent.replaceProperty(Property("UID", uid))
+    def modifyEvent(self, href, vtodo):
+        """Overriden by subclasses"""
+        pass
 
-            rrule = self._recurrenceDistribution.sample()
-            if rrule is not None:
-                vevent.addProperty(Property(None, None, None, pycalendar=rrule))
 
-            vevent.addProperty(self._client._makeSelfOrganizer())
-            vevent.addProperty(self._client._makeSelfAttendee())
+class TaskTitler(TaskUpdaterBase, Titler):
+    """
+    Changes the SUMMARY of a random VTODO
+    """
+    def modifyEvent(self, _ignore_href, vtodo):
+        vtodo.replaceProperty(Property("SUMMARY", "." * 5))
+        return "update{title}"
 
-            attendees = list(vevent.properties('ATTENDEE'))
-            for _ignore in range(int(self._inviteeCountDistribution.sample())):
-                try:
-                    self._addAttendee(vevent, attendees)
-                except CannotAddAttendee:
-                    self._failedOperation("invite", "Cannot add attendee")
-                    return succeed(None)
+class TaskNoter(TaskUpdaterBase, Noter):
+    """
+    Changes the NOTES of a random VTODO
+    """
+    def modifyEvent(self, _ignore_href, vtodo):
+        vtodo.replaceProperty(Property("DESCRIPTION", "." * 5))
+        return "update{notes}"
 
-            href = '%s%s.ics' % (calendar.url, uid)
-            d = self._client.addInvite(href, vcalendar)
-            return self._newOperation("invite", d)
 
+# class TaskAlerterMixin = AlerterMixin (alarm AND due)
+# self._taskStartDistribution = taskDueDistribution
+# vtodo.replaceProperty(Property("DUE", due))
 
+
+class Prioritizer(TaskUpdaterBase):
+    PRIORITY_NONE = 0
+    PRIORITY_HIGH = 1
+    PRIORITY_MEDIUM = 5
+    PRIORITY_LOW = 9
+
+    def setDistributions(
+        self,
+        priorityDistribution=UniformDiscreteDistribution([
+            PRIORITY_NONE, PRIORITY_LOW, PRIORITY_MEDIUM, PRIORITY_HIGH
+        ])
+    ):
+        self._priority = priorityDistribution
+
+    def modifyEvent(self, _ignore_href, vtodo):
+        self._setPriority(vtodo, self._priority.sample())
+
+
+    def _setPriority(self, vtodo, priority):
+        """ Set the PRIORITY of a VTODO """
+        vtodo.replaceProperty(Property("PRIORITY", priority))
+
+class Completer(TaskUpdaterBase):
+    def setDistributions(
+        self,
+        completeLikelihood=BernoulliDistribution(0.9)
+    ):
+        self._complete = completeLikelihood
+
+    def modifyEvent(self, _ignore_href, vtodo):
+        if self._complete.sample():
+            self._markTaskComplete(vtodo)
+        else:
+            self._markTaskIncomplete(vtodo)
+
+    def _markTaskComplete(self, vtodo):
+        """ Mark a VTODO as complete """
+        vtodo.replaceProperty(Property("COMPLETED", DateTime.getNowUTC()))
+        vtodo.replaceProperty(Property("PERCENT-COMPLETE", 100))
+        vtodo.replaceProperty(Property("STATUS", "COMPLETED"))
+
+    def _markTaskIncomplete(self, vtodo):
+        """ Mark a VTODO as incomplete """
+        vtodo.removeProperty("COMPLETED")
+        vtodo.removeProperty("PERCENT-COMPLETE")
+        vtodo.replaceProperty(Property("STATUS", "NEEDS-ACTION"))
+
+
 ##########################
 # Notification Behaviors #
 ##########################
@@ -1151,7 +847,7 @@
     A Calendar user who accepts invitations to events. As well as accepting requests, this
     will also remove cancels and replies.
     """
-    def setParameters(
+    def setDistributions(
         self,
         enabled=True,
         acceptDelayDistribution=NormalDistribution(1200, 60)
@@ -1298,107 +994,6 @@
 
 
 
-class EventUpdater(EventerBase):
-    """
-    A Calendar user who creates a new event, and then updates its alarm.
-    """
-    def initialize(self):
-        """
-        Called before the profile runs for real. Can be used to initialize client state.
-
-        @return: a L{Deferred} that fires when initialization is done
-        """
-        self.action = self._updateEvent
-        return self._initEvent()
-
-
-    def _initEvent(self):
-        if not self._client.started:
-            return succeed(None)
-
-        # If it already exists, don't re-create
-        calendar = self._calendarsOfType(caldavxml.calendar, "VEVENT")[0]
-        if calendar.events:
-            events = [event for event in calendar.events.values() if event.url.endswith("event_to_update.ics")]
-            if events:
-                return succeed(None)
-
-        # Copy the template event and fill in some of its fields
-        # to make a new event to create on the calendar.
-        vcalendar = self._eventTemplate.duplicate()
-        vevent = vcalendar.mainComponent()
-        uid = str(uuid4())
-        dtstart = self._eventStartDistribution.sample()
-        dtend = dtstart + Duration(seconds=self._eventDurationDistribution.sample())
-        vevent.replaceProperty(Property("CREATED", DateTime.getNowUTC()))
-        vevent.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC()))
-        vevent.replaceProperty(Property("DTSTART", dtstart))
-        vevent.replaceProperty(Property("DTEND", dtend))
-        vevent.replaceProperty(Property("UID", uid))
-
-        rrule = self._recurrenceDistribution.sample()
-        if rrule is not None:
-            vevent.addProperty(Property(None, None, None, pycalendar=rrule))
-
-        href = '%s%s' % (calendar.url, "event_to_update.ics")
-        d = self._client.addEvent(href, vcalendar)
-        return self._newOperation("create", d)
-
-
-    def _updateEvent(self):
-        """
-        Try to add a new attendee to an event, or perhaps remove an
-        existing attendee from an event.
-
-        @return: C{None} if there are no events to play with,
-            otherwise a L{Deferred} which fires when the attendee
-            change has been made.
-        """
-
-        if not self._client.started:
-            return succeed(None)
-
-        # If it does not exist, try to create it
-        calendar = self._calendarsOfType(caldavxml.calendar, "VEVENT")[0]
-        if not calendar.events:
-            return self._initEvent()
-        events = [event for event in calendar.events.values() if event.url.endswith("event_to_update.ics")]
-        if not events:
-            return self._initEvent()
-        event = events[0]
-
-        # Add/update the ACKNOWLEDGED property
-        component = event.component.mainComponent()
-        component.replaceProperty(Property("ACKNOWLEDGED", DateTime.getNowUTC()))
-        d = self._client.changeEvent(event.url)
-        return self._newOperation("update", d)
-
-    # def _changeEventTitle(self, event, title):
-    #     event.component = self._setEventTitle(event.component, title)
-        # event.
-
-
-
-
-
-class EventDeleter(EventerBase):
-    """
-    A calendar user who deletes events at random
-    """
-    def initialize(self):
-        self.action = self._deleteEvent
-
-    def _deleteEvent(self):
-        event = self._getRandomEvent()
-        if event is None:
-            return succeed(None)
-        d = self._client.deleteEvent(event)
-        return self._newOperation("delete", d)
-
-
-
-
-
 ######################
 # Calendar Behaviors #
 ######################
@@ -1411,7 +1006,7 @@
         return succeed(None)
 
 
-    def setParameters(self, enabled=True, interval=25):
+    def setDistributions(self, enabled=True, interval=25):
         self.enabled = enabled
         self._interval = interval
 
@@ -1419,26 +1014,26 @@
 
 class CalendarMaker(CalendarBase):
     """ A Calendar user who adds new Calendars """
-    def initialize(self):
-        self.action = self._addCalendar
-        return succeed(None)
 
-
     def _addCalendar(self):
-        if not self._client.started:
-            return None
+        print "Adding a calendar"
+        # if not self._client.started:
+        #     return None
 
-        uid = str(uuid4())
+        # uid = str(uuid4())
 
-        body = Calendar.buildCalendarXML(order=0, component_type="VEVENT", rgba_color='FB524FFF', name='Sample Calendar')
-        print("Making new calendar with uid: " + uid)
-        # XXX Just for testing! remove this soon
-        path = "/calendars/__uids__/" + self._client.record.guid + "/" + uid + "/"
-        d = self._client.addCalendar(path, body)
+        # body = Calendar.buildCalendarXML(order=0, component_type="VEVENT", rgba_color='FB524FFF', name='Sample Calendar')
+        # print("Making new calendar with uid: " + uid)
+        # # XXX Just for testing! remove this soon
+        # path = "/calendars/__uids__/" + self._client.record.guid + "/" + uid + "/"
+        # d = self._client.addCalendar(path, body)
+        d = succeed('calendar created')
         return self._newOperation("create", d)
 
+    action = _addCalendar
 
 
+
 class CalendarUpdater(CalendarBase):
     """
     A calendar user who updates random calendars
@@ -1545,3 +1140,16 @@
         print("Deleting " + calendar.url)
         d = self._client.deleteCalendar(calendar.url)
         return self._newOperation("delete", d)
+
+if __name__ == '__main__':
+    class TestProfile(ProfileBase):
+        def sayHello(self):
+            print("Hello!")
+        action = sayHello
+
+    from twisted.internet import reactor
+
+    profile = TestProfile(enabled=True, interval=1)
+    profile.setUp(reactor, None, None, None)
+    profile.run()
+    reactor.run()

Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/push.py
===================================================================
--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/push.py	2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/push.py	2015-08-31 20:37:45 UTC (rev 15081)
@@ -1,6 +1,10 @@
-from calendarserver.push.amppush import subscribeToIDs
-from twisted.internet.defer import succeed
+import uuid
 
+from twisted.internet.endpoints import TCP4ClientEndpoint
+from twisted.internet.defer import inlineCallbacks
+
+from calendarserver.push.amppush import SubscribeToID, UnsubscribeFromID, AMPPushClientFactory
+
 class PushMonitor(object):
     """
     Watchguard that monitors push notifications (AMP Push)
@@ -21,71 +25,107 @@
         @param ampPushPort: AMP port to connect to (e.g. 62311)
         @type ampPushPort: integer
         @param callback: a one-argument function that is fired
-            with a calendar hrefupon receipt of a push notification
+            with a calendar href upon receipt of a push notification
             for that resource
         @type callback: one-argument callable
         """
+
+        if reactor is None:
+            from twisted.internet import reactor
+
         self._reactor = reactor
         self._ampPushHost = ampPushHost
         self._ampPushPort = ampPushPort
 
         # Keep track of AMP parameters for calendar homes we encounter.  This
-        # dictionary has calendar home URLs as keys and pushkeys as
-        # values.
+        # dictionary has pushkeys as keys and calendar home URLs as values.
         self._ampPushkeys = {}
 
+        self._callback = callback
+
+        self._token = str(uuid.uuid4()) # Unique token for this monitor
+        self._endpoint = TCP4ClientEndpoint(self._reactor, self._ampPushHost, self._ampPushPort)
+        self._factory = AMPPushClientFactory(self._receivedAMPPush)
+        self._connected = False
+
+    @inlineCallbacks
     def begin(self):
         """
         Start monitoring for AMP-based push notifications
         """
-        self._subscribeToIDs(self.ampPushkeys)
+        self._protocol = yield self._endpoint.connect(self._factory)
+        self._connected = True
+        pushkeys = self._ampPushkeys.keys()
+        yield self._subscribeToPushkeys(pushkeys)
 
+    @inlineCallbacks
     def end(self):
         """
-        Finish monitoring push notifications. Any other cleanup should be done here
+        Finish monitoring push notifications.
         """
-        self._unsubscribeFromAll()
+        pushkeys = self._ampPushkeys.keys()
+        self._ampPushkeys = {}
+        yield self._unsubscribeFromPushkeys(pushkeys)
 
-    def _subscribeToIDs(self, ids):
+        # Close the connection between client and server
+        yield self._protocol.transport.loseConnection()
+        self._connected = False
 
-        subscribeToIDs(
-            self._ampPushHost,
-            self._ampPushPort,
-            ids,
-            self._receivedAmpPush,
-            self._reactor
-        )
 
-    def _receivedAMPPush(self, inboundID, dataChangedTimestamp, priority=5):
-        print("-" * 64)
-        print("{} received a PUSH with ID={}, timestamp={}, priority={}".format(self.record.commonName, inboundID, dataChangedTimestamp, priority))
-        print("By the way, my AMP keys are {}".format(self._ampPushkeys))
-        print("-" * 64)
+    def addPushkey(self, pushkey, href):
+        """
+        Register a pushkey associated with a specific calendar href.
 
-        for href, calendar_id in self.ampPushkeys.iteritems():
-            if inboundID == calendar_id:
-                self.callback(href)
-                break
-        else:
-            # Somehow we are not subscribed to this inboundID
-            print("*" * 16 + "Oh no - we're not subscribed to " + str(inboundID) + " but we received a notification anyway!")
-            pass
+        @param pushkey: AMP pushkey returned by the server, used to listen to notifications
+        @type pushkey: C{str}
+        @param href: href of calendar home set. When the server triggers a push for the
+            associated pushkey, the callback will be fired with this href
+        @type href: C{str}
 
-    def _unsubscribeFromAll(self):
-        # For now, the server doesn't support unsubscribing from pushkeys, so we simply
-        # "forget" about our registered pushkeys
-        self._ampPushkeys = {}
+        Example Usage:
+            monitor.addPushkey('/CalDAV/localhost/<uid>', '/calendars/__uids__/<uid>')
+        """
+        self._ampPushkeys[pushkey] = href
+        if self._connected:
+            return self._subscribeToPushkey(pushkey)
 
+    def removePushkey(self, pushkey):
+        """
+        Unregister the calendar home associated with the specified pushkey
+        """
+        if pushkey in self._ampPushkeys:
+            del self._ampPushkeys[pushkey]
+        if self._connected:
+            return self._unsubscribeFromPushkey(pushkey)
 
-    def addPushkey(self, href, pushkey):
-        self._ampPushkeys[href] = pushkey
-        self.subscribeToIDs()
+    def isSubscribedTo(self, href):
+        """
+        Returns true if and only if the given calendar href is actively being monitored
+        """
+        return href in self._ampPushkeys.itervalues()
 
+    @inlineCallbacks
+    def _subscribeToPushkeys(self, pushkeys):
+        for pushkey in pushkeys:
+            yield self._subscribeToPushkey(pushkey)
 
-    def removePushkey(self, pushkey):
-        # if self.ampPushkeys.has_value(pushkey):
-        #     del self.ampPushKeys
-        pass
+    @inlineCallbacks
+    def _unsubscribeFromPushkeys(self, pushkeys):
+        for pushkey in pushkeys:
+            yield self._unsubscribeFromPushkey(pushkey)
 
-    def isSubscribedTo(self, href):
-        return href in self.ampPushkeys
+    def _subscribeToPushkey(self, pushkey):
+        return self._protocol.callRemote(SubscribeToID, token=self._token, id=pushkey)
+
+    def _unsubscribeFromPushkey(self, pushkey):
+        return self._protocol.callRemote(UnsubscribeFromID, id=pushkey)
+
+
+    def _receivedAMPPush(self, inboundID, dataChangedTimestamp, priority=5):
+        if inboundID in self._ampPushkeys:
+            # Only react if we're tracking this pushkey
+            href = self._ampPushkeys[inboundID]
+            self._callback(href)
+        else:
+            # Somehow we are not subscribed to this pushkey
+            pass

Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/records.py
===================================================================
--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/records.py	2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/records.py	2015-08-31 20:37:45 UTC (rev 15081)
@@ -11,6 +11,19 @@
     def __repr__(self):
         return "Record(%s:%s %s %s %s)" % (self.uid, self.password, self.commonName, self.email, self.guid)
 
+# def generateRecords(
+#     count, uidPattern="user%d", passwordPattern="user%d",
+#     namePattern="User %d", emailPattern="user%d at example.com",
+#     guidPattern="user%d"
+# ):
+#     for i in xrange(count):
+#         i += 1
+#         uid = uidPattern % (i,)
+#         password = passwordPattern % (i,)
+#         name = namePattern % (i,)
+#         email = emailPattern % (i,)
+#         guid = guidPattern % (i,)
+#         yield DirectoryRecord(uid, password, name, email, guid)
 
 def recordsFromCSVFile(path):
     if path:

Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/requester.py
===================================================================
--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/requester.py	2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/requester.py	2015-08-31 20:37:45 UTC (rev 15081)
@@ -24,7 +24,7 @@
 class Requester(object):
     """
     Utility to create requests on behalf of a client. Public methods are:
-    method     url     body     headers     status     method_label 
+    method     url     body     headers     status     method_label
     ------------------------------------------------------------------------
     GET        req     ---
     POST       req     req
@@ -100,6 +100,7 @@
             client_id=self._client_id,
         )
 
+
         before = self._reactor.seconds()
         response = yield self._agent.request(method, url, headers, StringProducer(body) if body else None)
 

Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/resources.py
===================================================================
--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/resources.py	2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/resources.py	2015-08-31 20:37:45 UTC (rev 15081)
@@ -114,13 +114,6 @@
         self.url = url
         self.changeToken = changeToken
         self.events = {}
-        # print("----\nNew Calendar")
-        # print("Resource Type: ", self.resourceType)
-        # print("Component Types: ", self.componentTypes)
-        # print("Name: ", self.name)
-        # print("URL: ", self.url)
-        # print("Change Token: ", self.changeToken)
-        # print("Events: ", self.events)
 
 
     def serialize(self):
@@ -156,20 +149,7 @@
                 calendar.changeToken = ""
         return calendar
 
-    @staticmethod
-    def buildCalendarXML(order=0, component_type='VEVENT', rgba_color='FB524FFF', name='Simple Calendar'):
-        # TODO add timezone information
 
-        # MakeCalendar(None, '/', name, )
-
-        # body = _STARTUP_CREATE_CALENDAR.format(
-        #     order=order,
-        #     component_type=component_type,
-        #     color=rgba_color,
-        #     name=name)
-        # return body
-        return ""
-
     @staticmethod
     def addInviteeXML(uid, summary, readwrite=True):
         return AddInvitees(None, '/', [uid], readwrite, summary=summary).request_data.text
@@ -231,11 +211,3 @@
         el = ElementTree.Element(qn)
         el.text = order
         return Calendar._buildPropPatchXML(el)
-
-    # @inlineCallbacks
-    # def setCalendarProperty(self, calendar):
-
-    # def do_stuff(...):
-    #     body = 
-    #     yield self.requester.proppatch(href, body, method_label="PROPPATCH{calendar}")
-    #     
\ No newline at end of file

Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/clients.plist
===================================================================
--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/clients.plist	2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/clients.plist	2015-08-31 20:37:45 UTC (rev 15081)
@@ -38,7 +38,7 @@
 				<dict>
 					<!-- Name that appears in logs. -->
 					<key>title</key>
-					<string>10.11 Intern</string>
+					<string>10.11</string>
 
 					<!-- OS_X_10_7 can poll the calendar home at some interval. This is
 						in seconds. -->
@@ -64,7 +64,7 @@
 				<array>
 					<dict>
 						<key>class</key>
-						<string>contrib.performance.loadtest.profiles.Inviter</string>
+						<string>contrib.performance.loadtest.profiles.Eventer</string>
 
 						<key>params</key>
 						<dict>
@@ -74,7 +74,7 @@
 							<!-- Define the interval (in seconds) at which this profile will use
 								its client to create a new event. -->
 							<key>interval</key>
-							<integer>1</integer>
+							<integer>2</integer>
 						</dict>
 					</dict>
 				</array>

Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/settings/clients.py
===================================================================
--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/settings/clients.py	2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/settings/clients.py	2015-08-31 20:37:45 UTC (rev 15081)
@@ -1,35 +1,26 @@
 from contrib.performance.loadtest.clients import iOS_5, OS_X_10_6, OS_X_10_7, OS_X_10_11
-from contrib.performance.loadtest.profiles import CalendarMaker, CalendarUpdater, CalendarSharer, CalendarDeleter
-from contrib.performance.loadtest.population import ProfileType
+from contrib.performance.loadtest.profiles import (
+    Eventer, EventDeleter,
+    Titler,
+    Tasker, TaskDeleter,
+    TaskTitler, TaskNoter, Completer, Prioritizer,
 
-from preset_distributions import STANDARD_WORK_DISTRIBUTION, LOW_RECURRENCE_DISTRIBUTION, MEDIUM_RECURRENCE_DISTRIBUTION
+    CalendarMaker, CalendarUpdater, CalendarSharer, CalendarDeleter
+)
+from contrib.performance.loadtest.distributions import FixedDistribution, BernoulliDistribution
 
-# We have to roll our own deep copy method because you can't deep copy Twisted's reactor
-class ClientFactory(object):
 
-    def __init__(self, client, weight):
-        pass
+from preset_distributions import STANDARD_WORK_DISTRIBUTION, LOW_RECURRENCE_DISTRIBUTION, MEDIUM_RECURRENCE_DISTRIBUTION
 
-    @staticmethod
-    def _duplicateClient(client):
-        return type(client)(
-            # some params
-        )
-
-    def new(reactor, ):
-        pass
-
-class ProfileFactory(object):
-    def __init__(self, profile):
-        pass
-
-    @staticmethod
-    def _duplicateProfile(profile):
-        return type(profile)()
-
-calendars_only = [
+config = [
     {
         "software": OS_X_10_11,
+        #     title="10.11",
+        #     calendarHomePollInterval=5,
+        #     supportAmpPush=True,
+        #     ampPushHost="localhost",
+        #     ampPushPort62311
+        # )
         "params": {
             "title": "10.11",
             "calendarHomePollInterval": 5,
@@ -38,33 +29,41 @@
             "ampPushPort": 62311
         },
         "profiles": [
-            ProfileType(CalendarMaker, dict(enabled=True, interval=15)),
+            # Eventer(enabled=True, interval=0.01, eventStartDistribution=STANDARD_WORK_DISTRIBUTION),
+            # Titler(enabled=True, interval=1, titleLengthDistribution=FixedDistribution(10)),
 
-            # CalendarMaker(enabled=True, interval=15),
+            # Tasker(enabled=False, interval=1),
+            # Completer(enabled=True, interval=0.5, completeLikelihood=BernoulliDistribution(0.5)),
+            # Prioritizer(enabled=True, interval=0.1),
+            TaskTitler(enabled=True, interval=1),
+            TaskNoter(enabled=True, interval=1),
+            # TaskDeleter(enabled=True, interval=1),
+
+            # CalendarMaker(enabled=True, interval=1),
             # CalendarUpdater(enabled=True, interval=5),
             # CalendarSharer(enabled=True, interval=30),
             # CalendarDeleter(false=True, interval=30)
         ],
-        "weight": 1
+        "weight": 3
     }
 ]
 
-# TBD what about multiple weights?
-calendars_only_ideal = [
-    OS_X_10_11(
-        title="10.11",
-        calendarHomePollInterval=5,
-        supportAmpPush=True,
-        ampPushHost="localhost",
-        ampPushPort=62311,
-        profiles=[
-            CalendarMaker(enabled=True, interval=15),
-            # CalendarUpdater(enabled=True, interval=5),
-            # CalendarSharer(enabled=False, interval=30),
-            # CalendarDeleter(enabled=False, interval=30)
-        ]
-    )
-]
+# # TBD what about multiple weights?
+# calendars_only_ideal = [
+#     OS_X_10_11(
+#         title="10.11",
+#         calendarHomePollInterval=5,
+#         supportAmpPush=True,
+#         ampPushHost="localhost",
+#         ampPushPort=62311,
+#         profiles=[
+#             CalendarMaker(enabled=True, interval=15),
+#             # CalendarUpdater(enabled=True, interval=5),
+#             # CalendarSharer(enabled=False, interval=30),
+#             # CalendarDeleter(enabled=False, interval=30)
+#         ]
+#     )
+# ]
 
 # event_updates_only = [
 #     {
@@ -111,7 +110,3 @@
 #         "weight": 1
 #     }
 # ]
-
-
-# clientConfiguration = calendars_only
-# __all__ = [clientConfiguration]

Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/settings/config.py
===================================================================
--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/settings/config.py	2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/settings/config.py	2015-08-31 20:37:45 UTC (rev 15081)
@@ -1,22 +1,13 @@
-# Generally, the defaults are good enough for us.
+from contrib.performance.loadtest.settings.defaults import arrival, requestLogger, operationLogger, statisticsReporter, accounts
 
-config = dict(
-    server=server,
-    webadminPort=8080,
-    serverStatsPort=8100,
-    serializationPath='/tmp/sim',
-    arrival=arrival,
-    observers=[_requestLogger, _operationLogger, _statisticsReporter],
-    records=accounts
-)
+from contrib.performance.loadtest.logger import EverythingLogger, MessageLogger
 
-config_dist = dict(
-    server=server,
-    webadminPort=8080,
-    serverStatsPort=8100,
-    serializationPath='/tmp/sim',
-    arrival=arrival,
-    observers=[_requestLogger, _operationLogger, _statisticsReporter],
-    records=accounts,
-    workers=["./bin/python contrib/performance/loadtest/ampsim.py"] * 6,
-)
\ No newline at end of file
+config = {
+    "server": 'https://127.0.0.1:8443',
+    "webadminPort": 8080,
+    "serverStatsPort": {'server': 'localhost', 'Port': 8100},
+    "serializationPath": '/tmp/sim',
+    "arrival": arrival,
+    "observers": [requestLogger, operationLogger, statisticsReporter, EverythingLogger(), MessageLogger()],
+    "records": accounts
+}

Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/sim.py
===================================================================
--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/sim.py	2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/sim.py	2015-08-31 20:37:45 UTC (rev 15081)
@@ -48,8 +48,9 @@
 from contrib.performance.loadtest.clients import OS_X_10_6
 from contrib.performance.loadtest.profiles import Eventer, Inviter, Accepter
 from contrib.performance.loadtest.population import (
-    Populator, ProfileType, ClientType, PopulationParameters, SmoothRampUp,
+    ClientFactory, PopulationParameters, SmoothRampUp,
     CalendarClientSimulator)
+from contrib.performance.loadtest.config import Config
 from contrib.performance.loadtest.webadmin import LoadSimAdminResource
 
 def safeDivision(value, total, factor=1):
@@ -88,19 +89,30 @@
     """
     config = None
     settings = FilePath(__file__).sibling("settings")
-    _defaultConfig = settings.child("config.plist")
-    _defaultClients = settings.child("clients.plist")
+    # plists = settings.child('alt-settings').child('plist')
+    # _defaultConfig = plists.child("config.plist")
+    # _defaultClients = plists.child("clients.plist")
+    # _defaultConfig = settings.child('settings.config')
+    # _defaultClients = settings.child('settings.clients')
+    _defaultConfig = 'contrib.performance.loadtest.settings.config'
+    _defaultClients = 'contrib.performance.loadtest.settings.clients'
 
+    optFlags = [
+        ("debug", "d", "Enable Deferred and Failure debugging"),
+        ("debug-deferred", None, "Enable only Deferred debugging"),
+        ("debug-failure", None, "Enable only Failure debugging"),
+        ("use-plist", None, "Interpret configuration files as XML property lists (default false)")
+    ]
+
     optParameters = [
         ("runtime", "t", None,
          "Specify the limit (seconds) on the time to run the simulation.",
          int),
         ("config", None, _defaultConfig,
-         "Configuration plist file name from which to read simulation parameters.",
-         FilePath),
+         "Configuration plist file name from which to read simulation parameters."),
         ("clients", None, _defaultClients,
-         "Configuration plist file name from which to read client parameters.",
-         FilePath),
+         "Configuration plist file name from which to read client parameters."),
+        ("logfile", "l", '-', FilePath)
     ]
 
 
@@ -140,40 +152,48 @@
 
 
     def postOptions(self):
-        try:
-            configFile = self['config'].open()
-        except IOError, e:
-            raise UsageError("--config %s: %s" % (
-                self['config'].path, e.strerror))
-        try:
+        """
+        Convert the given configuration files to dictionaries, respectively in
+        self.config and self.clients
+        """
+        configPath = self["config"]
+        clientsPath = self["clients"]
+        if self['use-plist']:
             try:
-                self.config = readPlist(configFile)
+                with open(configPath) as configFile: # Could raise an IOError
+                    self.config = readPlist(configFile) # Could raise an ExpatError
+            except IOError, e:
+                raise UsageError("--config %s: %s" % (configPath.path, e.strerror))
             except ExpatError, e:
-                raise UsageError("--config %s: %s" % (self['config'].path, e))
-        finally:
-            configFile.close()
+                raise UsageError("--config %s: %s" % (configPath.path, e))
 
-        try:
-            # from importlib import import_module
-            # client_config = import_module("contrib.performance.loadtest.settings.clients")
-            clientFile = self['clients'].open()
-        except IOError, e:
-            raise UsageError("--clients %s: %s" % (
-                self['clients'].path, e.strerror))
-        try:
             try:
-                client_config = readPlist(clientFile)
-                # self.config["clients"] = client_config.calendars_only# client_config["clients"]
-                self.config["clients"] = client_config["clients"]
-                if "arrivalInterval" in client_config:
-                    self.config["arrival"]["params"]["interval"] = client_config["arrivalInterval"]
+                with open(clientsPath) as clientFile: # Could raise an IOError
+                    self.clients = readPlist(clientFile) # Could raise an ExpatError
+            except IOError, e:
+                raise UsageError("--clients %s: %s" % (clientsPath.path, e.strerror))
             except ExpatError, e:
-                raise UsageError("--clients %s: %s" % (self['clients'].path, e))
-        finally:
-            # clientFile.close()
-            pass
+                raise UsageError("--clients %s: %s" % (clientsPath.path, e))
 
+            self.config["clients"] = self.clients["clients"]
 
+        else:
+            from importlib import import_module
+            try:
+                config = import_module(configPath) # Could raise ImportError
+                self.config = config.config # Could raise attribute error
+            except (ImportError, AttributeError), e:
+                raise UsageError("--config %s: %s" % (configPath, e))
+            try:
+                clients = import_module(clientsPath) # Could raise ImportError
+                self.clients = clients.config # Could raise attribute error
+            except (ImportError, AttributeError), e:
+                raise UsageError("--clients %s: %s" % (clientsPath, e))
+
+        self.configObj = Config()
+        self.configObj.populateFrom(self.config, self.clients)
+
+
 Arrival = namedtuple('Arrival', 'factory parameters')
 
 
@@ -194,6 +214,8 @@
     def __init__(self, server, webadminPort, serverStats, serializationPath, arrival, parameters, observers=None,
                  records=None, reactor=None, runtime=None, workers=None,
                  configTemplate=None, workerID=None, workerCount=1):
+        if configTemplate == {}:
+            raise Exception('Got here!')
         if reactor is None:
             from twisted.internet import reactor
         self.server = server
@@ -224,7 +246,7 @@
         except UsageError, e:
             raise SystemExit(str(e))
 
-        return cls.fromConfig(options.config, options['runtime'], output)
+        return cls.fromConfigObject(options.configObj, options['runtime'], output)
 
 
     @classmethod
@@ -338,7 +360,6 @@
             observers=observers,
             records=records,
             runtime=runtime,
-            reactor=reactor,
             workers=workers,
             configTemplate=configTemplate,
             workerID=workerID,
@@ -348,138 +369,47 @@
 
     @classmethod
     def fromConfigObject(cls, config, runtime=None, output=stdout):
-        workers = config['workers']
-        if workers is None:
-            # Client / place where the simulator actually runs configuration
-            workerID = config.get("workerID", 0)
-            workerCount = config.get("workerCount", 1)
-            configTemplate = None
-            server = config.get('server', 'http://127.0.0.1:8008')
-            serializationPath = None
+        # if config.isManaging:
+        #     observers = 
 
-            serializationPath = config['serializationPath']
+        # if config.isWorking:
+        #     simulator = CalendarClientSimulator(
+        #         config.records,
+        #         config.parameters,
+        #         config.reactor,
+        #         config.server,
+        #         config.serializationPath,
+        #         config.workerID,
+        #         config.workerCount,
+        #     )
+        #     arrival = config.arrival
 
-            if 'arrival' in config:
-                arrival = Arrival(
-                    namedAny(config['arrival']['factory']),
-                    config['arrival']['params'])
-            else:
-                arrival = Arrival(
-                    SmoothRampUp, dict(groups=10, groupSize=1, interval=3))
-
-            parameters = PopulationParameters()
-            if 'clients' in config:
-                for clientConfig in config['clients']:
-                    parameters.addClient(
-                        clientConfig["weight"],
-                        ClientType(
-                            clientConfig["software"],
-                            clientConfig["params"],
-                            clientConfig["profiles"]
-                        )
-                    )
-                        # ClientType(
-                        #     namedAny(clientConfig["software"]),
-                        #     cls._convertParams(clientConfig["params"]),
-                        #     [
-                        #         ProfileType(
-                        #             namedAny(profile["class"]),
-                        #             cls._convertParams(profile["params"])
-                        #         ) for profile in clientConfig["profiles"]
-                        #     ]))
-            if not parameters.clients:
-                parameters.addClient(1,
-                                     ClientType(OS_X_10_6, {},
-                                                [Eventer, Inviter, Accepter]))
-        else:
-            # Manager / observer process.
-            server = ''
-            serializationPath = None
-            arrival = None
-            parameters = None
-            workerID = 0
-            configTemplate = config
-            workerCount = 1
-
-        # webadminPort = 
-        webadminPort = None
-        if 'webadmin' in config:
-            if config['webadmin']['enabled']:
-                webadminPort = config['webadmin']['HTTPPort']
-
-        serverStats = None
-        if 'serverStats' in config:
-            if config['serverStats']['enabled']:
-                serverStats = config['serverStats']
-                serverStats['server'] = config['server'] if 'server' in config else ''
-
-        observers = []
-        if 'observers' in config:
-            for observer in config['observers']:
-                observerName = observer["type"]
-                observerParams = observer["params"]
-                observers.append(namedAny(observerName)(**observerParams))
-
-        records = []
-        if 'accounts' in config:
-            loader = config['accounts']['loader']
-            params = config['accounts']['params']
-            records.extend(namedAny(loader)(**params))
-            output.write("Loaded {0} accounts.\n".format(len(records)))
-
         return cls(
-            server,
-            webadminPort,
-            serverStats,
-            serializationPath,
-            arrival,
-            parameters,
-            observers=observers,
-            records=records,
+            config.server,
+            config.webadminPort,
+            config.serverStats,
+            config.serializationPath,
+            config.arrival,
+            config.parameters,
+            observers=config.observers,
+            records=config.records,
             runtime=runtime,
-            reactor=reactor,
-            workers=workers,
-            configTemplate=configTemplate,
-            workerID=workerID,
-            workerCount=workerCount,
+            configTemplate=config.serializeForWorker,
+            workers=config.workers,
+            workerID=config.workerID,
+            workerCount=config.workerCount,
         )
 
-    @classmethod
-    def _convertParams(cls, params):
-        """
-        Find parameter values which should be more structured than plistlib is
-        capable of constructing and replace them with the more structured form.
 
-        Specifically, find keys that end with C{"Distribution"} and convert
-        them into some kind of distribution object using the associated
-        dictionary of keyword arguments.
-        """
-        for k, v in params.iteritems():
-            if k.endswith('Distribution'):
-                params[k] = cls._convertDistribution(v)
-        return params
-
-
     @classmethod
-    def _convertDistribution(cls, value):
-        """
-        Construct and return a new distribution object using the type and
-        params specified by C{value}.
-        """
-        return namedAny(value['type'])(**value['params'])
-
-
-    @classmethod
     def main(cls, args=None):
         simulator = cls.fromCommandLine(args)
         raise SystemExit(simulator.run())
 
 
     def createSimulator(self):
-        populator = Populator(Random())
         return CalendarClientSimulator(
             self.records,
-            populator,
             self.parameters,
             self.reactor,
             self.server,
@@ -491,7 +421,8 @@
 
     def createArrivalPolicy(self):
         # print(self.arrival.parameters)
-        return self.arrival.factory(self.reactor, **self.arrival.parameters)
+        # return zory(self.reactor, **self.arrival.parameters)
+        return self.arrival
 
 
     def serviceClasses(self):
@@ -503,6 +434,7 @@
             PrimaryService = WorkerSpawnerService
         else:
             PrimaryService = SimulatorService
+
         return [
             ObserverService,
             ReporterService,
@@ -575,6 +507,7 @@
             data = ""
             while not data.endswith("\n"):
                 d = s.recv(1024)
+                print("Received data from stats socket: ", d)
                 if d:
                     data += d
                 else:
@@ -622,6 +555,9 @@
         """
         super(ObserverService, self).startService()
         for obs in self.loadsim.observers:
+            import random
+            val = random.random()
+            msg(type='log', text='Adding observer: ' + obs.__class__.__name__, val=str(val))
             addObserver(obs.observe)
 
 
@@ -639,16 +575,19 @@
     """
 
     def startService(self):
+        print("Starting simulator service")
         super(SimulatorService, self).startService()
         self.clientsim = self.loadsim.createSimulator()
         arrivalPolicy = self.loadsim.createArrivalPolicy()
-        arrivalPolicy.run(self.clientsim)
+        arrivalPolicy.run(self.loadsim.reactor, self.clientsim)
+        # self.loadsim.arrival.run(self.loadsim.reactor, self.loadsim.simulator)
 
 
     @inlineCallbacks
     def stopService(self):
         yield super(SimulatorService, self).stopService()
         yield self.clientsim.stop()
+        # yield self.loadsim.simulator.stop()
 
 
 
@@ -662,6 +601,7 @@
         """
         Start observing.
         """
+        print("Starting reporter service")
         super(ReporterService, self).startService()
         self.loadsim.reporter = self
 
@@ -736,6 +676,7 @@
         super(WorkerSpawnerService, self).startService()
         self.bridges = []
         for workerID, worker in enumerate(self.loadsim.workers):
+            print("Building bridge for #" + str(workerID))
             bridge = ProcessProtocolBridge(
                 self, Manager(self.loadsim, workerID, len(self.loadsim.workers),
                               self.output)

Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/tests/test_distributions.py
===================================================================
--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/tests/test_distributions.py	2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/tests/test_distributions.py	2015-08-31 20:37:45 UTC (rev 15081)
@@ -1,25 +1,91 @@
 from twisted.trial.unittest import TestCase
 
 from contrib.performance.loadtest.distributions import (
-    LogNormalDistribution, UniformDiscreteDistribution,
-    UniformIntegerDistribution, WorkDistribution, RecurrenceDistribution
+    # Continuous distributions
+    LogNormalDistribution, NormalDistribution,
+    # Discrete distributions
+    UniformDiscreteDistribution, UniformIntegerDistribution,
+    BernoulliDistribution, BinomialDistribution, FixedDistribution,
+    # Calendar-specific distributions
+    WorkDistribution, RecurrenceDistribution,
 )
 
 from pycalendar.datetime import DateTime
 from pycalendar.timezone import Timezone
 
+from scipy import stats
+from scipy.optimize import curve_fit
+import itertools
+
+"""
+Disclaimer: These tests are nondeterministic, so be careful
+"""
+
 class DistributionTestBase(TestCase):
-    def getSamples(self, n):
+    def get_n_samples(self, dist, n):
         samples = []
         for _ignore_i in xrange(n):
-            samples.append()
+            samples.append(dist.sample())
+        return samples
 
-    def close(self, n):
-        pass
+class DiscreteDistributionTests(DistributionTestBase):
+    def test_bernoulli(self):
+        sample_count = 1000
+        proportions = [0, 0.1, 0.25, 0.5, 0.75, 0.9, 1]
+        for prop in proportions:
+            dist = BernoulliDistribution(proportion=prop)
+            samples = self.get_n_samples(dist, sample_count)
+            successes = samples.count(True)
 
-class DistributionTests(TestCase):
+            # This representes the likelihood that we would see as many successes
+            # as we did given that the true proportion is prop
+            p_value = stats.binom_test(successes, n=sample_count, p=prop)
+            self.assertFalse(p_value <= 0.01, "%d/%d, expected %f" % (successes, sample_count, prop))
 
+    def test_binomial(self):
+        sample_counts = [100, 1000, 10000]
+        proportions = [0, 0.1, 0.25, 0.5, 0.75, 0.9, 1]
+        for sample_count, prop in itertools.product(sample_counts, proportions):
+            dist = BinomialDistribution(p=prop, n=sample_count)
+            successes = dist.sample()
 
+            # This representes the likelihood that we would see as many successes
+            # as we did given that the true proportion is prop
+            p_value = stats.binom_test(successes, n=sample_count, p=prop)
+            self.assertFalse(p_value <= 0.01, "%d/%d, expected %f" % (successes, sample_count, prop))
+
+    def test_fixed(self):
+        dist = FixedDistribution(4) # https://xkcd.com/221/
+        for _ignore_i in xrange(100):
+            self.assertEqual(dist.sample(), 4)
+
+    def test_uniformdiscrete(self):
+        population = [82, 101, 100, 109, 111, 110, 100]
+        counts = dict.fromkeys(population, 0)
+        dist = UniformDiscreteDistribution(population)
+        for _ignore_i in range(len(population) * 10):
+            counts[dist.sample()] += 1
+        self.assertEqual(dict.fromkeys(population, 10), counts)
+        # Do some chi squared stuff
+
+    def test_uniform(self):
+        dist = UniformIntegerDistribution(-5, 10)
+        for _ignore_i in range(100):
+            value = dist.sample()
+            self.assertTrue(-5 <= value < 10)
+            self.assertIsInstance(value, int)
+
+class ContinuousDistributionTests(TestCase):
+    def is_fit(self, pdf, xdata, ydata, pexp):
+        """
+        expected parameters
+        """
+        popt, pcov = curve_fit(pdf, xdata, ydata)
+        print popt
+
+    def test_normal(self):
+        dist = NormalDistribution()
+
     def test_lognormal(self):
         dist = LogNormalDistribution(mu=1, sigma=1)
         for _ignore_i in range(100):
@@ -48,16 +114,8 @@
         self.assertRaises(ValueError, LogNormalDistribution, mean=1)
         self.assertRaises(ValueError, LogNormalDistribution, median=1)
 
+class CalendarDistributionTests(TestCase):
 
-    def test_uniformdiscrete(self):
-        population = [1, 5, 6, 9]
-        counts = dict.fromkeys(population, 0)
-        dist = UniformDiscreteDistribution(population)
-        for _ignore_i in range(len(population) * 10):
-            counts[dist.sample()] += 1
-        self.assertEqual(dict.fromkeys(population, 10), counts)
-
-
     def test_workdistribution(self):
         tzname = "US/Eastern"
         dist = WorkDistribution(["mon", "wed", "thu", "sat"], 10, 20, tzname)
@@ -91,7 +149,7 @@
             self.assertTrue(value is None)
 
         dist = RecurrenceDistribution(True, {"daily": 1, "none": 2, "weekly": 1})
-        dist._helperDistribution = UniformDiscreteDistribution([0, 3, 2, 1, 0], randomize=False)
+        dist._helperDistribution = UniformDiscreteDistribution([0, 3, 2, 1, 0])
         value = dist.sample()
         self.assertTrue(value is not None)
         value = dist.sample()
@@ -102,11 +160,3 @@
         self.assertTrue(value is not None)
         value = dist.sample()
         self.assertTrue(value is not None)
-
-
-    def test_uniform(self):
-        dist = UniformIntegerDistribution(-5, 10)
-        for _ignore_i in range(100):
-            value = dist.sample()
-            self.assertTrue(-5 <= value < 10)
-            self.assertIsInstance(value, int)

Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/tests/test_push.py
===================================================================
--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/tests/test_push.py	2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/tests/test_push.py	2015-08-31 20:37:45 UTC (rev 15081)
@@ -1,15 +1,19 @@
 from twisted.trial.unittest import TestCase
+from twisted.internet.defer import inlineCallbacks
 
-from calendarserver.push.amppush import AMPPushMaster
-
 from contrib.performance.loadtest.push import PushMonitor
 
 class PushMonitorTests(TestCase):
-    def fakePush(self, inboundID, dataChangedTimestamp, priority=5):
-        self.monitor._receivedAMPPush(inboundID, dataChangedTimestamp, priority)
+    def sendFakePush(self, pushkey):
+        self.monitor._receivedAMPPush(inboundID=pushkey, dataChangedTimestamp=None, priority=None)
 
+    def receivedPush(self, calendar_href):
+        self.history.append(calendar_href)
+
     def setUp(self):
-        self.pushMaster = AMPPushMaster()
+        """
+        Creates and begins a PushMonitor with a history-tracking callback
+        """
 
         self.monitor = PushMonitor(
             None,
@@ -17,16 +21,94 @@
             62311,
             self.receivedPush
         )
-        self.monitor.begin()
+        self.history = []
+        return self.monitor.begin()
 
-    def sendNotification(self, href, pushkey):
-        self.pushMaster.notify(href, pushkey, None, None)
+    def test_noPushkey(self):
+        """
+        Monitor will not react to a push if there are no registered pushkeys
+        """
+        pushkey = '/CalDAV/abc/def/'
+        calendar_home = '/foo/bar/'
+        self.assertFalse(self.monitor.isSubscribedTo(calendar_home))
+        self.sendFakePush(pushkey)
+        self.assertEqual(self.history, [])
 
+    @inlineCallbacks
     def test_addPushkey(self):
-        pass
+        """
+        Adding a pushkey triggers a notification for the corresponding calendar home
+        """
+        pushkey = '/CalDAV/abc/def/'
+        calendar_home = '/foo/bar/'
+        yield self.monitor.addPushkey(pushkey, calendar_home)
+        self.assertTrue(self.monitor.isSubscribedTo(calendar_home))
+        self.sendFakePush(pushkey)
+        self.assertEqual(self.history, [calendar_home])
 
+    @inlineCallbacks
     def test_removePushkey(self):
-        pass
+        """
+        Pushkeys can be unregistered
+        """
+        pushkey = '/CalDAV/abc/def/'
+        calendar_home = '/foo/bar/'
+        yield self.monitor.addPushkey(pushkey, calendar_home)
+        self.assertTrue(self.monitor.isSubscribedTo(calendar_home))
+        yield self.monitor.removePushkey(pushkey)
+        self.assertFalse(self.monitor.isSubscribedTo(calendar_home))
 
+        self.sendFakePush(pushkey)
+        self.assertEqual(self.history, [])
+
+    @inlineCallbacks
+    def test_addDuplicatePushkeys(self):
+        """
+        Adding the same pushkey twice only registers it once
+        """
+        pushkey = '/CalDAV/abc/def/'
+        calendar_home = '/foo/bar/'
+        yield self.monitor.addPushkey(pushkey, calendar_home)
+        yield self.monitor.addPushkey(pushkey, calendar_home)
+        self.assertTrue(self.monitor.isSubscribedTo(calendar_home))
+        self.sendFakePush(pushkey)
+        self.assertEqual(self.history, [calendar_home])
+
+    @inlineCallbacks
+    def test_addOverridePushkeys(self):
+        """
+        Adding the same pushkey with a different calendar home
+        unregisters the original
+        """
+        pushkey = '/CalDAV/abc/def/'
+        calendar_home = '/foo/bar/'
+        calendar_home_2 = '/foo/baz/'
+        yield self.monitor.addPushkey(pushkey, calendar_home)
+        self.assertTrue(self.monitor.isSubscribedTo(calendar_home))
+        yield self.monitor.addPushkey(pushkey, calendar_home_2)
+        self.assertFalse(self.monitor.isSubscribedTo(calendar_home))
+        self.assertTrue(self.monitor.isSubscribedTo(calendar_home_2))
+
+        self.sendFakePush(pushkey)
+        self.assertEqual(self.history, [calendar_home_2])
+
+    @inlineCallbacks
+    def test_multiplePushkeys(self):
+        """
+        Monitor supports registering multiple pushkeys
+        """
+        pushkey = '/CalDAV/abc/def/'
+        pushkey_2 = '/CalDAV/abc/xyz/'
+        calendar_home = '/foo/bar/'
+        calendar_home_2 = '/foo/baz/'
+        yield self.monitor.addPushkey(pushkey, calendar_home)
+        self.assertTrue(self.monitor.isSubscribedTo(calendar_home))
+        yield self.monitor.addPushkey(pushkey_2, calendar_home_2)
+        self.assertTrue(self.monitor.isSubscribedTo(calendar_home_2))
+        self.sendFakePush(pushkey_2)
+        self.assertEqual(self.history, [calendar_home_2])
+        self.sendFakePush(pushkey)
+        self.assertEqual(self.history, [calendar_home_2, calendar_home])
+
     def tearDown(self):
-        self.monitor.end()
+        return self.monitor.end()

Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/tests/test_resources.py
===================================================================
--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/tests/test_resources.py	2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/tests/test_resources.py	2015-08-31 20:37:45 UTC (rev 15081)
@@ -37,4 +37,3 @@
         """
         event = Event(None, u'/bar/baz', u'etag')
         self.assertIdentical(event.getUID(), None)
-
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20150831/1bd838f8/attachment-0001.html>


More information about the calendarserver-changes mailing list