Added a separate program for launching client connections. Meant mainly for server devs at this point. It is primarily useful for stress-testing so as to get profiling data using e.g. cProfiler - running many clients with a short timestep -will- slow the server to a crawl! If using sqlite3 with 50+ clients you will also see the memory consumption gradually - our sqlite3 setup runs in RAM by default and these things create a lot of new stuff every timestep - make sure to shut it down before you run out of memory! Not sure if this says much about the server's actual behaviour with many players at this point, the usage pattern is hardly "realistic". Also, it's usually better to shut down the server before the dummyrunner.
This commit is contained in:
parent
44b5ae07ba
commit
f306c5a6a2
2 changed files with 401 additions and 0 deletions
253
src/utils/dummyrunner.py
Normal file
253
src/utils/dummyrunner.py
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
"""
|
||||
Dummy client runner
|
||||
|
||||
This module implements a stand-alone launcher for stress-testing
|
||||
an Evennia game. It will launch any number of fake clients. These
|
||||
clients will log into the server and start doing random operations.
|
||||
Customizing and weighing these operations differently depends on
|
||||
which type of game is tested. The module contains a testing module
|
||||
for plain Evennia.
|
||||
|
||||
Please note that you shouldn't run this on a production server!
|
||||
Launch the program without any arguments or options to see a
|
||||
full step-by-step setup help.
|
||||
|
||||
Basically (for testing default Evennia):
|
||||
|
||||
- Use an empty/testing database.
|
||||
- set PERMISSION_PLAYERS_DEFAULT = "Builders"
|
||||
- start server, eventually with profiling active
|
||||
- launch this client runner
|
||||
|
||||
If you want to customize the runner's client actions
|
||||
(because you changed the cmdset or needs to better
|
||||
match your use cases or add more actions), you can
|
||||
change which actions by adding a path to
|
||||
|
||||
DUMMYRUNNER_ACTIONS_MODULE = <path.to.your.module>
|
||||
|
||||
in your settings. See utils.dummyrunner_actions.py
|
||||
for instructions on how to define this module.
|
||||
|
||||
"""
|
||||
|
||||
import os, sys, time, random
|
||||
from optparse import OptionParser
|
||||
from twisted.conch import telnet
|
||||
from twisted.internet import reactor, protocol
|
||||
# from twisted.application import internet, service
|
||||
# from twisted.web import client
|
||||
from twisted.internet.task import LoopingCall
|
||||
|
||||
# Tack on the root evennia directory to the python path and initialize django settings
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
from django.core.management import setup_environ
|
||||
from game import settings
|
||||
setup_environ(settings)
|
||||
|
||||
from django.conf import settings
|
||||
from src.utils import utils
|
||||
|
||||
HELPTEXT = """
|
||||
DO NOT RUN THIS ON A PRODUCTION SERVER! USE A CLEAN/TESTING DATABASE!
|
||||
|
||||
This stand-alone program launches dummy telnet clients against a
|
||||
running Evennia server. The idea is to mimic real players logging in
|
||||
and repeatedly doing resource-heavy commands so as to stress test the
|
||||
game. It uses the default command set to log in and issue commands, so
|
||||
if that was customized, some of the functionality will not be tested
|
||||
(it will not fail, the commands will just not be recognized). The
|
||||
running clients will create new objects and rooms all over the place
|
||||
as part of their running, so using a clean/testing database is
|
||||
strongly recommended.
|
||||
|
||||
Setup:
|
||||
1) setup a fresh/clean database (if using sqlite, just safe-copy
|
||||
away your real evennia.db3 file and create a new one with
|
||||
manage.py)
|
||||
2) in game/settings.py, add
|
||||
|
||||
PERMISSION_PLAYER_DEFAULT="Builders"
|
||||
|
||||
3a) Start Evennia like normal.
|
||||
3b) If you want profiling, start Evennia like this instead:
|
||||
|
||||
python runner.py -S start
|
||||
|
||||
this will start Evennia under cProfiler with output server.prof.
|
||||
4) run this dummy runner:
|
||||
|
||||
python dummyclients.py <nr_of_clients> [timestep] [port]
|
||||
|
||||
Default is to connect one client to port 4000, using a 5 second
|
||||
timestep. Increase the number of clients and shorten the
|
||||
timestep (minimum is 1s) to further stress the game.
|
||||
|
||||
You can stop the dummy runner with Ctrl-C.
|
||||
|
||||
5) Log on and determine if game remains responsive despite the
|
||||
heavier load. Note that if you do profiling, there is an
|
||||
additional overhead from the profiler too!
|
||||
6) If you use profiling, let the game run long enough to gather
|
||||
data, then stop the server. You can inspect the server.prof file
|
||||
from a python prompt (see Python's manual on cProfiler).
|
||||
|
||||
"""
|
||||
#------------------------------------------------------------
|
||||
# Helper functions
|
||||
#------------------------------------------------------------
|
||||
|
||||
def idcounter():
|
||||
"generates subsequent id numbers"
|
||||
idcount = 0
|
||||
while True:
|
||||
idcount += 1
|
||||
yield idcount
|
||||
OID = idcounter()
|
||||
CID = idcounter()
|
||||
|
||||
def makeiter(obj):
|
||||
"makes everything iterable"
|
||||
if not hasattr(obj, '__iter__'):
|
||||
return [obj]
|
||||
return obj
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Client classes
|
||||
#------------------------------------------------------------
|
||||
|
||||
class DummyClient(telnet.StatefulTelnetProtocol):
|
||||
"""
|
||||
Handles connection to a running Evennia server,
|
||||
mimicking a real player by sending commands on
|
||||
a timer.
|
||||
"""
|
||||
|
||||
def connectionMade(self):
|
||||
|
||||
# public properties
|
||||
self.cid = CID.next()
|
||||
self.istep = 0
|
||||
self.exits = [] # exit names created
|
||||
self.objs = [] # obj names created
|
||||
|
||||
self._actions = self.factory.actions
|
||||
self._echo_brief = self.factory.verbose == 1
|
||||
self._echo_all = self.factory.verbose == 2
|
||||
print " ** client %i connected successfully." % self.cid
|
||||
|
||||
# start client tick
|
||||
d = LoopingCall(self.step)
|
||||
d.start(self.factory.timestep, now=True).addErrback(self.error)
|
||||
|
||||
def dataReceived(self, data):
|
||||
"Echo incoming data to stdout"
|
||||
if self._echo_all:
|
||||
print data
|
||||
|
||||
def connectionLost(self, reason):
|
||||
"loosing the connection"
|
||||
print " ** client %i lost connection." % self.cid
|
||||
|
||||
def error(self, err):
|
||||
"error callback"
|
||||
print err
|
||||
|
||||
def counter(self):
|
||||
"produces a unique id, also between clients"
|
||||
return OID.next()
|
||||
|
||||
def step(self):
|
||||
"""
|
||||
Perform a step. This is called repeatedly by the runner
|
||||
and causes the client to issue commands to the server.
|
||||
This holds all "intelligence" of the dummy client.
|
||||
"""
|
||||
if self.istep == 0:
|
||||
cfunc = self._actions[0]
|
||||
else: # random selection using cumulative probabilities
|
||||
rand = random.random()
|
||||
cfunc = [func for cprob, func in self._actions[1] if cprob >= rand][0]
|
||||
# launch the action (don't hide tracebacks)
|
||||
cmd, report = cfunc(self)
|
||||
# handle the result
|
||||
cmd = "\n".join(makeiter(cmd))
|
||||
if self._echo_brief or self._echo_all:
|
||||
print "client %i %s" % (self.cid, report)
|
||||
self.sendLine(cmd)
|
||||
self.istep += 1
|
||||
|
||||
class DummyFactory(protocol.ClientFactory):
|
||||
protocol = DummyClient
|
||||
|
||||
def __init__(self, actions, timestep, verbose):
|
||||
"Setup the factory base (shared by all clients)"
|
||||
self.actions = actions
|
||||
self.timestep = timestep
|
||||
self.verbose = verbose
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Access method:
|
||||
# Starts clients and connects them to a running server.
|
||||
#------------------------------------------------------------
|
||||
|
||||
def start_all_dummy_clients(actions, nclients=1, timestep=5, telnet_port=4000, verbose=0):
|
||||
|
||||
# validating and preparing the action tuple
|
||||
|
||||
# make sure the probabilities add up to 1
|
||||
pratio = 1.0 / sum(tup[0] for tup in actions[1:])
|
||||
flogin, probs, cfuncs = actions[0], [tup[0] * pratio for tup in actions[1:]], [tup[1] for tup in actions[1:]]
|
||||
# create cumulative probabilies for the random actions
|
||||
cprobs = [sum(v for i,v in enumerate(probs) if i<=k) for k in range(len(probs))]
|
||||
# rebuild a new, optimized action structure
|
||||
actions = (flogin, zip(cprobs, cfuncs))
|
||||
|
||||
# setting up all clients (they are automatically started)
|
||||
factory = DummyFactory(actions, timestep, verbose)
|
||||
for i in range(nclients):
|
||||
reactor.connectTCP("localhost", telnet_port, factory)
|
||||
# start reactor
|
||||
reactor.run()
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Command line interface
|
||||
#------------------------------------------------------------
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
# parsing command line with default vals
|
||||
parser = OptionParser(usage="%prog [options] <nclients> [timestep, [port]]",
|
||||
description="This program requires some preparations to run properly. Start it without any arguments or options for full help.")
|
||||
parser.add_option('-v', '--verbose', action='store_const', const=1, dest='verbose',
|
||||
default=0,help="echo brief description of what clients do every timestep.")
|
||||
parser.add_option('-V', '--very-verbose', action='store_const',const=2, dest='verbose',
|
||||
default=0,help="echo all client returns to stdout (hint: use only with nclients=1!)")
|
||||
|
||||
options, args = parser.parse_args()
|
||||
|
||||
nargs = len(args)
|
||||
nclients = 1
|
||||
timestep = 5
|
||||
port = 4000
|
||||
try:
|
||||
if not args : raise Exception
|
||||
if nargs > 0: nclients = max(1, int(args[0]))
|
||||
if nargs > 1: timestep = max(1, int(args[1]))
|
||||
if nargs > 2: port = int(args[2])
|
||||
except Exception:
|
||||
print HELPTEXT
|
||||
sys.exit()
|
||||
|
||||
# import the ACTION tuple from a given module
|
||||
try:
|
||||
action_modpath = settings.DUMMYRUNNER_ACTIONS_MODULE
|
||||
except AttributeError:
|
||||
# use default
|
||||
action_modpath = "src.utils.dummyrunner_actions"
|
||||
actions = utils.mod_import(action_modpath, "ACTIONS")
|
||||
|
||||
print "Connecting %i dummy client(s) to port %i using a %i second timestep ... " % (nclients, port, timestep)
|
||||
start_all_dummy_clients(actions, nclients, timestep, port,
|
||||
verbose=options.verbose)
|
||||
print "... dummy client runner finished."
|
||||
Loading…
Add table
Add a link
Reference in a new issue