#! /usr/bin/env python
|
|
|
|
import random
|
|
from optparse import OptionParser
|
|
import string
|
|
|
|
def tprint(str):
|
|
print str
|
|
|
|
def dprint(str):
|
|
return
|
|
|
|
def dospace(howmuch):
|
|
for i in range(howmuch + 1):
|
|
print '%28s' % ' ',
|
|
|
|
# given list, pick random element and return it
|
|
def pickrand(tlist):
|
|
n = int(random.random() * len(tlist))
|
|
p = tlist[n]
|
|
return p
|
|
|
|
# given number, conclude if nth bit is set
|
|
def isset(num, index):
|
|
mask = 1 << index
|
|
return (num & mask) > 0
|
|
|
|
# useful instead of assert
|
|
def zassert(cond, str):
|
|
if cond == False:
|
|
print 'ABORT::', str
|
|
exit(1)
|
|
|
|
#
|
|
# Which files are used in the simulation
|
|
#
|
|
# Not representing a realistic piece of anything
|
|
# but rather just for convenience when generating
|
|
# random traces ...
|
|
#
|
|
# Files are named 'a', 'b', etc. for ease of use
|
|
# Could probably add a numeric aspect to allow
|
|
# for more than 26 files but who cares
|
|
#
|
|
|
|
class files:
|
|
def __init__(self, numfiles):
|
|
self.numfiles = numfiles
|
|
self.value = 0
|
|
self.filelist = list(string.ascii_lowercase)[0:numfiles]
|
|
|
|
def getfiles(self):
|
|
return self.filelist
|
|
|
|
def getvalue(self):
|
|
rc = self.value
|
|
self.value += 1
|
|
return rc
|
|
|
|
#
|
|
# Models the actions of the AFS server
|
|
#
|
|
# The only real interactions are get/put
|
|
# get() causes the server to track which files cache what;
|
|
# put() may cause callbacks to invalidate client caches
|
|
#
|
|
class server:
|
|
def __init__(self, files, solve, detail):
|
|
self.files = files
|
|
self.solve = solve
|
|
self.detail = detail
|
|
|
|
flist = self.files.getfiles()
|
|
self.contents = {}
|
|
for f in flist:
|
|
v = self.files.getvalue()
|
|
self.contents[f] = v
|
|
self.getcnt, self.putcnt = 0, 0
|
|
|
|
def stats(self):
|
|
print 'Server -- Gets:%d Puts:%d' % (self.getcnt, self.putcnt)
|
|
|
|
def filestats(self, printcontents):
|
|
for fname in self.contents:
|
|
if printcontents:
|
|
print('file:%s contains:%d' % (fname, self.contents[fname]))
|
|
else:
|
|
print('file:%s contains:?' % fname)
|
|
|
|
|
|
def setclients(self, clients):
|
|
# need list of clients
|
|
self.clients = clients
|
|
|
|
# per client callback list
|
|
self.cache = {}
|
|
for c in self.clients:
|
|
self.cache[c.getname()] = []
|
|
|
|
def get(self, client, fname):
|
|
zassert(fname in self.contents, 'server:get() -- file:%s not found on server' % fname)
|
|
self.getcnt += 1
|
|
if self.solve and isset(self.detail, 0):
|
|
print('getfile:%s c:%s [%d]' % (fname, client, self.contents[fname]))
|
|
if fname not in self.cache[client]:
|
|
self.cache[client].append(fname)
|
|
# dprint(' -> List for client %s' % client, ' is ', self.cache[client])
|
|
return self.contents[fname]
|
|
|
|
def put(self, client, fname, value):
|
|
zassert(fname in self.contents, 'server:put() -- file:%s not found on server' % fname)
|
|
self.putcnt += 1
|
|
self.contents[fname] = value
|
|
if self.solve and isset(self.detail, 0):
|
|
print('putfile:%s c:%s [%s]' % (fname, client, self.contents[fname]))
|
|
# scan others for callback
|
|
for c in self.clients:
|
|
cname = c.getname()
|
|
if fname in self.cache[cname] and cname != client:
|
|
if self.solve and isset(self.detail, 1):
|
|
print 'callback: c:%s file:%s' % (cname, fname)
|
|
c.invalidate(fname)
|
|
self.cache[cname].remove(fname)
|
|
|
|
#
|
|
# Per-client file descriptors
|
|
#
|
|
# Would be useful if the simulation allowed more
|
|
# than one active file open() at a time; it kind
|
|
# of does but this isn't really utilized
|
|
#
|
|
class filedesc:
|
|
def __init__(self, max=1024):
|
|
self.max = max
|
|
self.fd = {}
|
|
for i in range(self.max):
|
|
self.fd[i] = ''
|
|
|
|
def alloc(self, fname, sfd=-1):
|
|
if sfd != -1:
|
|
zassert(self.fd[sfd] == '', 'filedesc:alloc() -- fd:%d already in use, cannot allocate' % sfd)
|
|
self.fd[sfd] = fname
|
|
return sfd
|
|
else:
|
|
for i in range(self.max):
|
|
if self.fd[i] == '':
|
|
self.fd[i] = fname
|
|
return i
|
|
return -1
|
|
|
|
def lookup(self, sfd):
|
|
zassert(i >= 0 and i < self.max, 'filedesc:lookup() -- file descriptor out of valid range (%d not between 0 and %d)' % (sfd, self.max))
|
|
zassert(self.fd[sfd] != '', 'filedesc:lookup() -- fd:%d not in use, cannot lookup' % sfd)
|
|
return self.fd[sfd]
|
|
|
|
def free(self, i):
|
|
zassert(i >= 0 and i < self.max, 'filedesc:free() -- file descriptor out of valid range (%d not between 0 and %d)' % (sfd, self.max))
|
|
zassert(self.fd[sfd] != '', 'filedesc:free() -- fd:%d not in use, cannot free' % sfd)
|
|
self.fd[i] = ''
|
|
|
|
#
|
|
# The client cache
|
|
#
|
|
# Just models what files are cached.
|
|
# When a file is opened, its contents are fetched
|
|
# from the server and put in the cache. At that point,
|
|
# the cache contents are VALID, DIRTY/NOT (depending
|
|
# on whether this is for reading or writing), and the
|
|
# REFERENCE COUNT is set to 1. If multiple open's take
|
|
# place on this file, REFERENCE COUNT will be updated
|
|
# accordingly. VALID gets set to 0 if the cache is
|
|
# invalidated by a callback; however, the contents
|
|
# still might be used by a given client if the file
|
|
# is already open. Note that a callback does NOT
|
|
# prevent a client from overwriting an already opened file.
|
|
#
|
|
class cache:
|
|
def __init__(self, name, num, solve, detail):
|
|
self.name = name
|
|
self.num = num
|
|
self.solve = solve
|
|
self.detail = detail
|
|
|
|
self.cache = {}
|
|
|
|
self.hitcnt = 0
|
|
self.misscnt = 0
|
|
self.invalidcnt = 0
|
|
|
|
def stats(self):
|
|
print ' Cache -- Hits:%d Misses:%d Invalidates:%d' % (self.hitcnt, self.misscnt, self.invalidcnt)
|
|
|
|
def put(self, fname, data, dirty, refcnt):
|
|
self.cache[fname] = dict(data=data, dirty=dirty, refcnt=refcnt, valid=True)
|
|
|
|
def update(self, fname, data):
|
|
self.cache[fname] = dict(data=data, dirty=True, refcnt=self.cache[fname]['refcnt'], valid=self.cache[fname]['valid'])
|
|
|
|
def invalidate(self, fname):
|
|
zassert(fname in self.cache, 'cache:invalidate() -- cannot invalidate file not in cache (%s)' % fname)
|
|
self.invalidcnt += 1
|
|
self.cache[fname] = dict(data=self.cache[fname]['data'], dirty=self.cache[fname]['dirty'],
|
|
refcnt=self.cache[fname]['refcnt'], valid=False)
|
|
if self.solve and isset(self.detail, 1):
|
|
dospace(self.num)
|
|
if isset(self.detail,3):
|
|
print '%2s invalidate %s' % (self.name, fname)
|
|
else:
|
|
print 'invalidate %s' % (fname)
|
|
self.printstate(self.num)
|
|
|
|
def checkvalid(self, fname):
|
|
zassert(fname in self.cache, 'cache:checkvalid() -- cannot checkvalid on file not in cache (%s)' % fname)
|
|
if self.cache[fname]['valid'] == False and self.cache[fname]['refcnt'] == 0:
|
|
del self.cache[fname]
|
|
|
|
def printstate(self, fname):
|
|
for fname in self.cache:
|
|
data = self.cache[fname]['data']
|
|
dirty = self.cache[fname]['dirty']
|
|
refcnt = self.cache[fname]['refcnt']
|
|
valid = self.cache[fname]['valid']
|
|
if valid == True:
|
|
validPrint = 1
|
|
else:
|
|
validPrint = 0
|
|
if dirty == True:
|
|
dirtyPrint = 1
|
|
else:
|
|
dirtyPrint = 0
|
|
|
|
if self.solve and isset(self.detail, 2):
|
|
dospace(self.num)
|
|
if isset(self.detail, 3):
|
|
print '%s [%s:%2d (v=%d,d=%d,r=%d)]' % (self.name, fname, data, validPrint, dirtyPrint, refcnt)
|
|
else:
|
|
print '[%s:%2d (v=%d,d=%d,r=%d)]' % (fname, data, validPrint, dirtyPrint, refcnt)
|
|
|
|
def checkget(self, fname):
|
|
if fname in self.cache:
|
|
self.cache[fname] = dict(data=self.cache[fname]['data'], dirty=self.cache[fname]['dirty'],
|
|
refcnt=self.cache[fname]['refcnt'], valid=self.cache[fname]['valid'])
|
|
self.hitcnt += 1
|
|
return (True, self.cache[fname])
|
|
self.misscnt += 1
|
|
return (False, -1)
|
|
|
|
def get(self, fname):
|
|
assert(fname in self.cache)
|
|
return (True, self.cache[fname])
|
|
|
|
def incref(self, fname):
|
|
assert(fname in self.cache)
|
|
self.cache[fname] = dict(data=self.cache[fname]['data'], dirty=self.cache[fname]['dirty'],
|
|
refcnt=self.cache[fname]['refcnt'] + 1, valid=self.cache[fname]['valid'])
|
|
|
|
def decref(self, fname):
|
|
assert(fname in self.cache)
|
|
self.cache[fname] = dict(data=self.cache[fname]['data'], dirty=self.cache[fname]['dirty'],
|
|
refcnt=self.cache[fname]['refcnt'] - 1, valid=self.cache[fname]['valid'])
|
|
|
|
def setdirty(self, fname, dirty):
|
|
assert(fname in self.cache)
|
|
self.cache[fname] = dict(data=self.cache[fname]['data'], dirty=dirty,
|
|
refcnt=self.cache[fname]['refcnt'], valid=self.cache[fname]['valid'])
|
|
|
|
def setclean(self, fname):
|
|
assert(fname in self.cache)
|
|
self.cache[fname] = dict(data=self.cache[fname]['data'], dirty=False,
|
|
refcnt=self.cache[fname]['refcnt'], valid=self.cache[fname]['valid'])
|
|
|
|
def isdirty(self, fname):
|
|
assert(fname in self.cache)
|
|
return (self.cache[fname]['dirty'] == True)
|
|
|
|
def setvalid(self, fname):
|
|
assert(fname in self.cache)
|
|
self.cache[fname] = dict(data=self.cache[fname]['data'], dirty=self.cache[fname]['dirty'],
|
|
refcnt=self.cache[fname]['refcnt'], valid=True)
|
|
|
|
|
|
# actions
|
|
MICRO_OPEN = 1
|
|
MICRO_READ = 2
|
|
MICRO_WRITE = 3
|
|
MICRO_CLOSE = 4
|
|
|
|
def op2name(op):
|
|
if op == MICRO_OPEN:
|
|
return 'MICRO_OPEN'
|
|
elif op == MICRO_READ:
|
|
return 'MICRO_READ'
|
|
elif op == MICRO_WRITE:
|
|
return 'MICRO_WRITE'
|
|
elif op == MICRO_CLOSE:
|
|
return 'MICRO_CLOSE'
|
|
else:
|
|
abort('error: bad op -> ' + op)
|
|
|
|
#
|
|
# Client class
|
|
#
|
|
# Models the behavior of each client in the system.
|
|
#
|
|
#
|
|
#
|
|
class client:
|
|
def __init__(self, name, cid, server, files, bias, numsteps, actions, solve, detail):
|
|
self.name = name # readable name of client
|
|
self.cid = cid # client ID
|
|
self.server = server # server object
|
|
self.files = files # files object
|
|
self.bias = bias # bias
|
|
self.actions = actions # schedule exactly?
|
|
self.solve = solve # show answers?
|
|
self.detail = detail # how much of an answer to show
|
|
|
|
# cache
|
|
self.cache = cache(self.name, self.cid, self.solve, self.detail)
|
|
|
|
# file desc
|
|
self.fd = filedesc()
|
|
|
|
# stats
|
|
self.readcnt = 0
|
|
self.writecnt = 0
|
|
|
|
# init actions
|
|
self.done = False # track state
|
|
self.acnt = 0 # this is used when running
|
|
self.acts = [] # this just tracks the opcodes
|
|
|
|
if self.actions == '':
|
|
# in case with no specific actions, generate one...
|
|
for i in range(numsteps):
|
|
fname = pickrand(self.files.getfiles())
|
|
r = random.random()
|
|
fd = self.fd.alloc(fname)
|
|
zassert(fd >= 0, 'client:init() -- ran out of file descriptors, sorry!')
|
|
if r < self.bias[0]:
|
|
# FILE_READ
|
|
self.acts.append((MICRO_OPEN, fname, fd))
|
|
self.acts.append((MICRO_READ, fd))
|
|
self.acts.append((MICRO_CLOSE, fd))
|
|
else:
|
|
# FILE_WRITE
|
|
self.acts.append((MICRO_OPEN, fname, fd))
|
|
self.acts.append((MICRO_WRITE, fd))
|
|
self.acts.append((MICRO_CLOSE, fd))
|
|
else:
|
|
# in this case, unpack actions and make it happen
|
|
# should look like this: "oa1:ra1:ca1" (open 'a' for reading with file desc 1, read from file a (fd:1), etc.)
|
|
# yes the file descriptor and file name are redundant for read/write and close
|
|
for a in self.actions.split(':'):
|
|
act = a[0]
|
|
if act == 'o':
|
|
zassert(len(a) == 3, 'client:init() -- malformed open action (%s) should be oa1 or something like that' % a)
|
|
fname, fd = a[1], int(a[2])
|
|
self.fd.alloc(fname, fd)
|
|
assert(fd >= 0)
|
|
self.acts.append((MICRO_OPEN, fname, fd))
|
|
elif act == 'r':
|
|
zassert(len(a) == 2, 'client:init() -- malformed read action (%s) should be r1 or something like that' % a)
|
|
fd = int(a[1])
|
|
self.acts.append((MICRO_READ, fd))
|
|
elif act == 'w':
|
|
zassert(len(a) == 2, 'client:init() -- malformed write action (%s) should be w1 or something like that' % a)
|
|
fd = int(a[1])
|
|
self.acts.append((MICRO_WRITE, fd))
|
|
elif act == 'c':
|
|
zassert(len(a) == 2, 'client:init() -- malformed close action (%s) should be c1 or something like that' % a)
|
|
fd = int(a[1])
|
|
self.acts.append((MICRO_CLOSE, fd))
|
|
else:
|
|
print 'Unrecognized command: %s (from %s)' % (act, a)
|
|
exit(1)
|
|
# debug ACTS
|
|
# print self.acts
|
|
|
|
def getname(self):
|
|
return self.name
|
|
|
|
def stats(self):
|
|
print '%s -- Reads:%d Writes:%d' % (self.name, self.readcnt, self.writecnt)
|
|
self.cache.stats()
|
|
|
|
def getfile(self, fname, dirty):
|
|
(incache, item) = self.cache.checkget(fname)
|
|
if incache == True and item['valid'] == 1:
|
|
dprint(' -> CLIENT %s:: HAS LOCAL COPY of %s' % (self.name, fname))
|
|
self.cache.setdirty(fname, dirty)
|
|
else:
|
|
data = self.server.get(self.name, fname)
|
|
self.cache.put(fname, data, dirty, 0)
|
|
self.cache.incref(fname)
|
|
|
|
def putfile(self, fname, value):
|
|
self.server.put(self.name, fname, value)
|
|
self.cache.setclean(fname)
|
|
self.cache.setvalid(fname)
|
|
|
|
def invalidate(self, fname):
|
|
self.cache.invalidate(fname)
|
|
|
|
def step(self, space):
|
|
if self.done == True:
|
|
return -1
|
|
if self.acnt == len(self.acts):
|
|
self.done = True
|
|
return 0
|
|
|
|
# now figure out what to do and do it
|
|
# action, fname, fd = self.acts[self.acnt]
|
|
action = self.acts[self.acnt][0]
|
|
|
|
# print ''
|
|
# print '*************************'
|
|
# print '%s ACTION -> %s' % (self.name, op2name(action))
|
|
# print '*************************'
|
|
|
|
# first, do spacing for command (below)
|
|
dospace(space)
|
|
|
|
if isset(self.detail, 3) == True:
|
|
print self.name,
|
|
|
|
# now handle the action
|
|
if action == MICRO_OPEN:
|
|
fname, fd = self.acts[self.acnt][1], self.acts[self.acnt][2]
|
|
tprint('open:%s [fd:%d]' % (fname, fd))
|
|
self.getfile(fname, dirty=False)
|
|
elif action == MICRO_READ:
|
|
fd = self.acts[self.acnt][1]
|
|
fname = self.fd.lookup(fd)
|
|
self.readcnt += 1
|
|
incache, contents = self.cache.get(fname)
|
|
assert(incache == True)
|
|
if self.solve:
|
|
tprint('read:%d -> %d' % (fd, contents['data']))
|
|
else:
|
|
tprint('read:%d -> value?' % (fd))
|
|
elif action == MICRO_WRITE:
|
|
fd = self.acts[self.acnt][1]
|
|
fname = self.fd.lookup(fd)
|
|
self.writecnt += 1
|
|
incache, contents = self.cache.get(fname)
|
|
assert(incache == True)
|
|
v = self.files.getvalue()
|
|
self.cache.update(fname, v)
|
|
if self.solve:
|
|
tprint('write:%d %d -> %d' % (fd, contents['data'], v))
|
|
else:
|
|
tprint('write:%d value? -> %d' % (fd, v))
|
|
elif action == MICRO_CLOSE:
|
|
fd = self.acts[self.acnt][1]
|
|
fname = self.fd.lookup(fd)
|
|
incache, contents = self.cache.get(fname)
|
|
assert(incache == True)
|
|
tprint('close:%d' % (fd))
|
|
if self.cache.isdirty(fname):
|
|
self.putfile(fname, contents['data'])
|
|
self.cache.decref(fname)
|
|
self.cache.checkvalid(fname)
|
|
|
|
# useful to see
|
|
self.cache.printstate(self.name)
|
|
|
|
if self.solve and self.detail > 0:
|
|
print ''
|
|
|
|
# return that there is more left to do
|
|
self.acnt += 1
|
|
return 1
|
|
|
|
|
|
#
|
|
# main program
|
|
#
|
|
parser = OptionParser()
|
|
parser.add_option('-s', '--seed', default=0, help='the random seed', action='store', type='int', dest='seed')
|
|
parser.add_option('-C', '--clients', default=2, help='number of clients', action='store', type='int', dest='numclients')
|
|
parser.add_option('-n', '--numsteps', default=2, help='ops each client will do', action='store', type='int', dest='numsteps')
|
|
parser.add_option('-f', '--numfiles', default=1, help='number of files in server', action='store', type='int', dest='numfiles')
|
|
parser.add_option('-r', '--readratio', default=0.5, help='ratio of reads/writes', action='store', type='float', dest='readratio')
|
|
parser.add_option('-A', '--actions', default='', help='client actions exactly specified, e.g., oa1:r1:c1,oa1:w1:c1 specifies two clients; each opens the file a, client 0 reads it whereas client 1 writes it, and then each closes it',
|
|
action='store', type='string', dest='actions')
|
|
parser.add_option('-S', '--schedule', default='', help='exact schedule to run; 01 alternates round robin between clients 0 and 1. Left unspecified leads to random scheduling',
|
|
action='store', type='string', dest='schedule')
|
|
parser.add_option('-p', '--printstats', default=False, help='print extra stats', action='store_true', dest='printstats')
|
|
parser.add_option('-c', '--compute', default=False, help='compute answers for me', action='store_true', dest='solve')
|
|
parser.add_option('-d', '--detail', default=0, help='detail level when giving answers (1:server actions,2:invalidations,4:client cache,8:extra labels); OR together for multiple', action='store', type='int', dest='detail')
|
|
(options, args) = parser.parse_args()
|
|
|
|
print 'ARG seed', options.seed
|
|
print 'ARG numclients', options.numclients
|
|
print 'ARG numsteps', options.numsteps
|
|
print 'ARG numfiles', options.numfiles
|
|
print 'ARG readratio', options.readratio
|
|
print 'ARG actions', options.actions
|
|
print 'ARG schedule', options.schedule
|
|
print 'ARG detail', options.detail
|
|
print ''
|
|
|
|
seed = int(options.seed)
|
|
numclients = int(options.numclients)
|
|
numsteps = int(options.numsteps)
|
|
numfiles = int(options.numfiles)
|
|
readratio = float(options.readratio)
|
|
actions = options.actions
|
|
schedule = options.schedule
|
|
printstats = options.printstats
|
|
solve = options.solve
|
|
detail = options.detail
|
|
|
|
# with specific schedule, files are all specified by a single letter in specific actions list
|
|
# but we ignore this for now...
|
|
|
|
zassert(numfiles > 0 and numfiles <= 26, 'main: can only simulate 26 or fewer files, sorry')
|
|
zassert(readratio >= 0.0 and readratio <= 1.0, 'main: read ratio must be between 0 and 1 inclusive')
|
|
|
|
# start it
|
|
random.seed(seed)
|
|
|
|
# files in server to begin with
|
|
f = files(numfiles)
|
|
|
|
# make server
|
|
s = server(f, solve, detail)
|
|
|
|
clients = []
|
|
|
|
if actions != '':
|
|
# if specific actions are specified, figure some stuff out now
|
|
# e.g., oa1:ra1:ca1,oa1:ra1:ca1 which is list of 0's actions, then 1's, then...
|
|
cactions = actions.split(',')
|
|
if numclients != len(cactions):
|
|
numclients = len(cactions)
|
|
i = 0
|
|
for clist in cactions:
|
|
clients.append(client('c%d' % i, i, s, f, [], len(clist), clist, solve, detail))
|
|
i += 1
|
|
else:
|
|
# else, make random clients
|
|
for i in range(numclients):
|
|
clients.append(client('c%d' % i, i, s, f, [readratio, 1.0], numsteps, '', solve, detail))
|
|
|
|
# tell server about these clients
|
|
s.setclients(clients)
|
|
|
|
# init print out for clients
|
|
print '%12s' % 'Server', '%12s' % ' ',
|
|
for c in clients:
|
|
print '%13s' % c.getname(), '%13s' % ' ',
|
|
print ''
|
|
|
|
# main loop
|
|
#
|
|
# over time, pick a random client
|
|
# have it do one thing, show what happens
|
|
# move on to next and so forth
|
|
|
|
s.filestats(True)
|
|
|
|
# for use with specific schedule
|
|
schedcurr = 0
|
|
|
|
# check for legal schedule (must include all clients)
|
|
if schedule != '':
|
|
for i in range(len(clients)):
|
|
cnt = 0
|
|
for j in range(len(schedule)):
|
|
curr = schedule[j]
|
|
if int(curr) == i:
|
|
cnt += 1
|
|
zassert(cnt != 0, 'main: client %d not in schedule:%s, which would never terminate' % (i, schedule))
|
|
|
|
# RUN the schedule (either random or specified by user)
|
|
numrunning = len(clients)
|
|
while numrunning > 0:
|
|
if schedule == '':
|
|
c = pickrand(clients)
|
|
else:
|
|
idx = int(schedule[schedcurr])
|
|
# print 'SCHEDULE DEBUG:: schedule:', schedule, 'schedcurr', schedcurr, 'index', idx
|
|
c = clients[idx]
|
|
schedcurr += 1
|
|
if schedcurr == len(schedule):
|
|
schedcurr = 0
|
|
rc = c.step(clients.index(c))
|
|
if rc == 0:
|
|
numrunning -= 1
|
|
|
|
|
|
s.filestats(solve)
|
|
|
|
if printstats:
|
|
s.stats()
|
|
for c in clients:
|
|
c.stats()
|
|
|