r0c/test/stress.py

746 lines
16 KiB
Python
Raw Normal View History

2018-01-08 00:51:19 +00:00
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from __future__ import print_function
"""stress.py: retr0chat stress tester"""
__version__ = "0.9"
__author__ = "ed <a@ocv.me>"
__credits__ = ["stackoverflow.com"]
__license__ = "MIT"
__copyright__ = 2018
2018-01-09 01:10:09 +00:00
# config
#
2018-08-30 22:19:44 +00:00
#NUM_CLIENTS = 1
NUM_CLIENTS = 4
2018-08-30 22:19:44 +00:00
#CHANNELS = ['#1']
CHANNELS = ['#1','#2','#3','#4']
2018-01-16 01:11:48 +00:00
EVENT_DELAY = 0.01
EVENT_DELAY = 0.001
2018-01-16 01:11:48 +00:00
EVENT_DELAY = None
2018-01-21 21:38:48 +00:00
ITERATIONS = 1000000
ITERATIONS = 10000000
IMMEDIATE_TX = True
#IMMEDIATE_TX = False
2018-01-29 03:30:48 +00:00
VISUAL_CLIENT = True
#VISUAL_CLIENT = False
TELNET = False
TELNET = True
#
2018-01-09 01:10:09 +00:00
# config end
import multiprocessing
2018-01-08 00:51:19 +00:00
import threading
import asyncore
import socket
import signal
import random
2018-01-08 00:51:19 +00:00
import time
import sys
2018-01-29 03:30:48 +00:00
import os
sys.path.insert(1, os.path.join(sys.path[0], '..'))
from r0c.util import *
import builtins
try: print = __builtin__.print
except: print = builtins.print
2018-01-08 00:51:19 +00:00
PY2 = (sys.version_info[0] == 2)
if PY2:
from Queue import Queue
else:
from queue import Queue
2018-01-29 03:30:48 +00:00
def get_term_size():
"""
https://github.com/chrippa/backports.shutil_get_terminal_size
MIT licensed
"""
import struct
try:
from ctypes import windll, create_string_buffer, WinError
_handle_ids = {
0: -10,
1: -11,
2: -12,
}
def _get_terminal_size(fd):
handle = windll.kernel32.GetStdHandle(_handle_ids[fd])
if handle == 0:
raise OSError('handle cannot be retrieved')
if handle == -1:
raise WinError()
csbi = create_string_buffer(22)
res = windll.kernel32.GetConsoleScreenBufferInfo(handle, csbi)
if res:
res = struct.unpack("hhhhHhhhhhh", csbi.raw)
left, top, right, bottom = res[5:9]
columns = right - left + 1
lines = bottom - top + 1
return [columns, lines]
else:
raise WinError()
except ImportError:
import fcntl
import termios
def _get_terminal_size(fd):
try:
res = fcntl.ioctl(fd, termios.TIOCGWINSZ, b"\x00" * 4)
except IOError as e:
raise OSError(e)
lines, columns = struct.unpack("hh", res)
return [columns, lines]
try:
columns = int(os.environ["COLUMNS"])
except (KeyError, ValueError):
columns = 0
try:
lines = int(os.environ["LINES"])
except (KeyError, ValueError):
lines = 0
# Only query if necessary
if columns <= 0 or lines <= 0:
try:
size = _get_terminal_size(sys.__stdout__.fileno())
except (NameError, OSError):
size = [80,24]
if columns <= 0:
columns = size[0]
if lines <= 0:
lines = size[1]
return [columns, lines]
tsz = get_term_size()
tsz[1] -= 1
tszb = struct.pack('>HH', *tsz)
#print(b2hex(tszb))
#sys.exit(0)
2018-01-08 00:51:19 +00:00
class Client(asyncore.dispatcher):
def __init__(self, core, port, behavior, status_q):
2018-01-08 00:51:19 +00:00
asyncore.dispatcher.__init__(self)
self.core = core
self.port = port
self.behavior = behavior
self.status_q = status_q
self.explain = True
2018-01-09 00:07:30 +00:00
self.explain = False
self.dead = False
self.stopping = False
self.actor_active = False
self.bootup()
def bootup(self):
2018-01-08 00:51:19 +00:00
self.in_text = u''
2018-01-16 01:11:48 +00:00
self.tx_only = False
self.outbox = Queue(1000)
2018-01-08 00:51:19 +00:00
self.backlog = None
2018-01-16 01:11:48 +00:00
self.num_outbox = 0
self.num_sent = 0
self.pkt_sent = 0
2018-01-08 00:51:19 +00:00
self.stage = 'start'
self.nick = 'x'
2018-01-08 00:51:19 +00:00
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.connect(('127.0.0.1', self.port))
thr = threading.Thread(target=self.actor)
thr.daemon = True # for emergency purposes
thr.start()
def send_status(self, txt):
if False:
print(txt)
self.status_q.put(txt)
def actor(self):
2018-01-16 01:11:48 +00:00
print_stages = False
self.actor_active = True
print('actor going up')
2018-01-09 01:10:09 +00:00
while not self.stopping:
time.sleep(0.02)
if self.stage == 'start':
self.send_status('start')
2018-01-29 03:30:48 +00:00
hit = False
if 'verify that your previous config' in self.in_text:
hit = True
self.in_text = u''
self.tx('n')
if 'type the text below, then hit [Enter]:' in self.in_text:
2018-01-29 03:30:48 +00:00
hit = True
self.stage = 'qwer'
self.in_text = u''
for ch in u'qwer asdf\n':
self.tx(ch)
time.sleep(0.1)
2018-01-29 03:30:48 +00:00
if hit and TELNET:
#print('sending telnet termsize\n'*100)
self.txb(b'\xff\xfa\x1f' + tszb + b'\xff\xf0')
continue
if self.stage == 'qwer':
self.send_status('qwer')
test_pass = False
if 'your client is stuck in line-buffered mode' in self.in_text:
print('WARNING: r0c thinks we are linemode')
test_pass = True
self.tx(u'a')
2018-01-16 01:11:48 +00:00
if 'text appeared as you typed' in self.in_text:
test_pass = True
self.tx(u'b')
if test_pass:
self.in_text = u''
self.stage = 'color'
continue
2018-01-16 01:11:48 +00:00
if self.stage == 'color':
self.send_status('color')
if 'does colours work?' in self.in_text:
self.stage = 'codec'
self.in_text = u''
self.tx(u'y')
continue
if self.stage == 'codec':
self.send_status('codec')
if 'which line looks like' in self.in_text:
self.stage = 'getnick'
self.tx(u'a')
continue
if self.stage == 'getnick':
self.send_status('getnick')
ofs1 = self.in_text.find(u'H\033[0;36m')
ofs2 = self.in_text.find(u'>\033[0m ')
if ofs1 >= 0 and ofs2 == ofs1 + 8 + 6:
2018-01-29 03:30:48 +00:00
if not TELNET:
#print('sending vt100 termsize\n'*100)
self.tx(u'\033[{1};{0}R'.format(*tsz))
self.nick = self.in_text[ofs2-6:ofs2]
2018-01-09 00:07:30 +00:00
self.in_text = u''
self.tx(u'/join {0}\n'.format(CHANNELS[0]))
self.stage = 'ready'
self.send_status('{0}:start'.format(self.nick))
2018-01-16 01:11:48 +00:00
if self.behavior == 'flood_single_channel':
return self.flood_single_channel()
if self.behavior == 'jump_channels':
return self.jump_channels()
if self.behavior == 'reconnect_loop':
return self.reconnect_loop()
2018-01-29 03:30:48 +00:00
if self.behavior == 'split_utf8_runes':
return self.split_utf8_runes()
print('u wot')
return
self.actor_active = False
def flood_single_channel(self):
2018-01-09 01:10:09 +00:00
while not self.stopping:
time.sleep(0.02)
if self.stage == 'ready':
if 'fire/' in self.in_text:
self.stage = 'main'
self.in_Text = u''
continue
if self.stage == 'main':
self.stage = 'done'
for n in range(4000):
time.sleep(0.01)
self.tx(u'{0} {1}\n'.format(time.time(), n))
continue
if self.stage == 'done':
time.sleep(1)
self.text = u'' # dont care
if not self.outbox.empty():
continue
self.tx(u'{0} done\n'.format(time.time()))
self.actor_active = False
2018-01-29 03:30:48 +00:00
def split_utf8_runes(self):
charset = u'⢀⣴⣷⣄⠈⠻⡿⠋'
to_send = charset.encode('utf-8')
while False:
#print('tx up')
for n in range(len(to_send)):
#print('tx ch')
self.txb(to_send[n:n+1])
time.sleep(0.2)
self.txb(to_send[0:2])
to_send = to_send[2:] + to_send[0:2]
while True:
for n in range(0, len(to_send), 3):
self.txb(to_send[n:n+3])
time.sleep(0.2)
def reconnect_loop(self):
#print('reconnect_loop here')
channels_avail = CHANNELS
for chan in channels_avail:
self.tx(u'/join {0}\n'.format(chan))
time.sleep(1)
#print('reconnect_loop closing')
self.close()
#print('reconnect_loop booting')
self.bootup()
#print('reconnect_loop sayonara')
2018-01-09 00:07:30 +00:00
def expl(self, msg):
if not self.explain:
return
print(msg)
2018-01-09 00:07:30 +00:00
self.await_continue()
2018-01-09 00:07:30 +00:00
def await_continue(self):
self.in_text = u''
t0 = time.time()
2018-01-09 01:10:09 +00:00
while not self.stopping and not 'zxc mkl' in self.in_text:
2018-01-09 00:07:30 +00:00
time.sleep(0.1)
if time.time() - t0 > 10:
break
self.in_text = u''
def jump_channels(self):
immediate = IMMEDIATE_TX
delay = EVENT_DELAY
#print(immediate)
#sys.exit(0)
2018-01-16 01:11:48 +00:00
self.tx_only = immediate
script = []
active_chan = 0
member_of = [CHANNELS[0]]
channels_avail = CHANNELS
# maps to channels_avail
msg_id = [0]*len(channels_avail)
# ---- acts ----
# next channel
# join a channel
# part a channel
# send a message
chance = [ 10, 5, 4, 18 ]
chance = [ 10, 3, 2, 30 ]
2018-01-16 01:11:48 +00:00
chance = [ 10, 30, 2, 130 ]
for n in range(len(chance)-1):
chance[n+1] += chance[n]
print(chance)
#sys.exit(1)
odds_next, odds_join, odds_part, odds_send = chance
2018-01-21 21:38:48 +00:00
for n in range(ITERATIONS):
2018-01-09 01:10:09 +00:00
if self.stopping:
2018-01-09 00:07:30 +00:00
break
if n % 1000 == 0:
self.send_status('{0}:ev.{1}'.format(self.nick, n))
#self.tx(u'at event {0}\n'.format(n))
2018-01-09 01:10:09 +00:00
while not self.stopping:
2018-01-09 00:07:30 +00:00
if not member_of:
next_act = 13
else:
next_act = random.randrange(sum(chance))
2018-01-09 00:07:30 +00:00
if self.explain:
print('in [{0}], active [{1}:{2}], msgid [{3}], next [{4}]'.format(
','.join(member_of), active_chan, channels_avail[active_chan],
','.join(str(x) for x in msg_id), next_act))
if next_act <= odds_next:
2018-01-09 00:07:30 +00:00
if not member_of:
self.expl('tried to jump channel but we are all alone ;_;')
continue
changed_from_i = active_chan
changed_from_t = member_of[active_chan]
active_chan += 1
script.append(b'\x18')
if active_chan >= len(member_of):
# we do not consider the status channel
script.append(b'\x18')
active_chan = 0
changed_to_i = active_chan
changed_to_t = member_of[active_chan]
if self.explain:
2018-01-09 00:07:30 +00:00
self.expl('switching to next channel from {0} to {1} ({2} to {3})'.format(
changed_from_i, changed_to_i,
changed_from_t, changed_to_t))
for act in script:
2018-01-16 01:11:48 +00:00
self.txb(act)
self.txb(b'hello\n')
2018-01-09 00:07:30 +00:00
self.await_continue()
script = []
break
if next_act <= odds_join:
if len(member_of) == len(channels_avail):
2018-01-09 00:07:30 +00:00
self.expl('tried to join channel but filled {0} of {1} possible'.format(
len(member_of), len(channels_avail)))
# out of channels to join, try a different act
continue
while True:
to_join = random.choice(channels_avail)
if to_join not in member_of:
break
member_of.append(to_join)
active_chan = len(member_of) - 1
2018-01-09 00:07:30 +00:00
self.expl('going to join {0}:{1}, moving from {2}:{3}'.format(
len(member_of), to_join, active_chan, member_of[active_chan]))
script.append(u'/join {0}\n'.format(to_join).encode('utf-8'))
if self.explain:
for act in script:
2018-01-16 01:11:48 +00:00
self.txb(act)
self.txb(b'hello\n')
2018-01-09 00:07:30 +00:00
self.await_continue()
script = []
break
if next_act <= odds_part:
#continue
if not member_of:
2018-01-09 00:07:30 +00:00
self.expl('tried to leave channel but theres nothing to leave')
# out of channels to part, try a different act
continue
to_part = random.choice(member_of)
chan_idx = member_of.index(to_part)
2018-01-09 00:07:30 +00:00
self.expl('gonna leave {0}:{1}, we are in {2}:{3}'.format(
chan_idx, to_part, active_chan, member_of[active_chan]))
# jump to the channel to part from
while active_chan != chan_idx:
2018-01-09 00:07:30 +00:00
self.expl('jumping over from {0} to {1}'.format(
active_chan, active_chan + 1 ))
active_chan += 1
script.append(b'\x18')
if active_chan >= len(member_of):
2018-01-09 00:07:30 +00:00
self.expl('wraparound; dodging the status chan')
# we do not consider the status channel
script.append(b'\x18')
active_chan = 0
if active_chan == len(member_of) - 1:
2018-01-09 00:07:30 +00:00
self.expl('we are at the end of the channel list, decreasing int')
del member_of[active_chan]
active_chan -= 1
else:
2018-01-09 00:07:30 +00:00
self.expl('we are not at the end of the channel list, keeping it')
del member_of[active_chan]
2018-01-09 00:07:30 +00:00
if member_of:
self.expl('we will end up in {0}:{1}'.format(active_chan, member_of[active_chan]))
else:
self.expl('we have now left all our channels')
script.append(b'/part\n')
2018-01-09 00:07:30 +00:00
if self.explain:
for act in script:
2018-01-16 01:11:48 +00:00
self.txb(act)
self.txb(b'hello\n')
2018-01-09 00:07:30 +00:00
self.await_continue()
script = []
break
if not member_of:
# not in any channels, try a different act
continue
chan_name = member_of[active_chan]
chan_idx = channels_avail.index(chan_name)
msg_id[chan_idx] += 1
2018-01-09 00:07:30 +00:00
self.expl('gonna talk to {0}:{1}, msg #{2}'.format(
chan_idx, chan_name, msg_id[chan_idx]))
script.append(u'{0} {1} {2}\n'.format(
chan_name, msg_id[chan_idx], n).encode('utf-8'))
if self.explain:
for act in script:
2018-01-16 01:11:48 +00:00
self.txb(act)
self.txb(b'hello\n')
2018-01-09 00:07:30 +00:00
self.await_continue()
script = []
if immediate:
for action in script:
2018-01-16 01:11:48 +00:00
self.txb(action)
if delay:
time.sleep(delay)
script = []
2018-01-09 00:07:30 +00:00
break
2018-01-09 00:07:30 +00:00
self.tx(u'q\n')
2018-01-09 01:10:09 +00:00
while not self.stopping:
if 'fire/' in self.in_text:
break
time.sleep(0.01)
2018-01-09 01:10:09 +00:00
2018-01-16 01:11:48 +00:00
self.tx_only = True
for n, ev in enumerate(script):
2018-01-09 01:10:09 +00:00
if self.stopping:
break
if n % 100 == 0:
2018-01-09 00:07:30 +00:00
print('at event {0}\n'.format(n))
2018-01-16 01:11:48 +00:00
self.txb(ev)
if delay:
time.sleep(delay)
2018-01-09 00:07:30 +00:00
self.tx(u'done')
print('done')
self.actor_active = False
2018-01-08 00:51:19 +00:00
def handle_close(self):
self.dead = True
2018-01-29 03:30:48 +00:00
def handle_error(self):
whoops()
2018-01-08 00:51:19 +00:00
def tx(self, bv):
2018-01-16 01:11:48 +00:00
self.txb(bv.encode('utf-8'))
2018-01-16 01:11:48 +00:00
def txb(self, bv):
self.num_outbox += len(bv)
self.outbox.put(bv)
2018-01-08 00:51:19 +00:00
def readable(self):
return not self.dead
def writable(self):
return (self.backlog or not self.outbox.empty())
2018-01-08 00:51:19 +00:00
def handle_write(self):
msg = self.backlog
if not msg:
msg = self.outbox.get()
sent = self.send(msg)
self.backlog = msg[sent:]
2018-01-16 01:11:48 +00:00
self.num_sent += sent
self.pkt_sent += 1
if self.pkt_sent % 8192 == 8191:
#print('outbox {0} sent {1} queue {2}'.format(self.num_outbox, self.num_sent, self.num_outbox - self.num_sent))
self.send_status('{0}:s{1},q{2}'.format(self.nick, self.num_sent, self.num_outbox - self.num_sent))
2018-01-08 00:51:19 +00:00
def handle_read(self):
if self.dead:
print('!!! read when dead')
return
data = self.recv(8192)
if not data:
self.dead = True
return
2018-01-29 03:30:48 +00:00
if VISUAL_CLIENT:
print(data.decode('utf-8', 'ignore'))
2018-01-16 01:11:48 +00:00
if not self.tx_only:
self.in_text += data.decode('utf-8', 'ignore')
2018-01-09 00:07:30 +00:00
#if self.explain:
# print(self.in_text)
2018-01-08 00:51:19 +00:00
2018-01-09 01:10:09 +00:00
class SubCore(object):
def __init__(self, port, behavior, cmd_q, stat_q):
self.cmd_q = cmd_q
self.stat_q = stat_q
2018-01-09 01:10:09 +00:00
self.port = port
self.behavior = behavior
2018-01-09 01:10:09 +00:00
self.stopped = False
self.client = Client(self, self.port, self.behavior, stat_q)
2018-01-09 01:10:09 +00:00
def run(self):
2018-01-16 01:11:48 +00:00
timeout = 0.2
while self.cmd_q.empty():
2018-01-16 01:11:48 +00:00
asyncore.loop(timeout, count=2)
2018-01-09 01:10:09 +00:00
self.client.stopping = True
2018-01-09 01:10:09 +00:00
clean_shutdown = False
for n in range(0, 40): # 2sec
if not self.client.actor_active:
clean_shutdown = True
break
time.sleep(0.05)
self.client.close()
self.stopped = True
class ClientAPI(object):
def __init__(self, mproc, cmd_q, stat_q):
self.mproc = mproc
self.cmd_q = cmd_q
self.stat_q = stat_q
self.status = 'not.init'
def recv_status(self):
while not self.stat_q.empty():
self.status = self.stat_q.get()
2018-01-08 00:51:19 +00:00
class Core(object):
def __init__(self):
2018-01-09 00:07:30 +00:00
if len(sys.argv) < 2:
print('need 1 argument: telnet port')
sys.exit(1)
2018-01-09 01:10:09 +00:00
port = int(sys.argv[1])
self.stopping = False
self.asyncore_alive = False
2018-01-08 00:51:19 +00:00
signal.signal(signal.SIGINT, self.signal_handler)
2018-08-30 22:19:44 +00:00
behaviors = ['jump_channels'] * (NUM_CLIENTS)
2018-01-29 03:30:48 +00:00
#behaviors.append('reconnect_loop')
2018-08-30 22:19:44 +00:00
#behaviors = ['split_utf8_runes'] * (NUM_CLIENTS)
2018-01-09 01:10:09 +00:00
self.clients = []
for behavior in behaviors:
cmd_q = multiprocessing.Queue()
stat_q = multiprocessing.Queue()
mproc = multiprocessing.Process(target=self.new_subcore, args=(cmd_q, stat_q))
self.clients.append(ClientAPI(mproc, cmd_q, stat_q))
cmd_q.put(port)
cmd_q.put(behavior)
mproc.start()
def new_subcore(self, cmd_q, stat_q):
subcore = SubCore(cmd_q.get(), cmd_q.get(), cmd_q, stat_q)
2018-01-09 01:10:09 +00:00
subcore.run()
cmd_q.get()
def print_status(self):
msg = u''
for cli in self.clients:
msg += u'{0}, '.format(cli.status)
print(msg)
2018-01-08 00:51:19 +00:00
def run(self):
print(' * test is running')
2018-01-29 03:30:48 +00:00
print_status = not VISUAL_CLIENT
2018-01-08 00:51:19 +00:00
while not self.stopping:
for n in range(0,5):
time.sleep(0.1)
for cli in self.clients:
cli.recv_status()
if self.stopping:
break
2018-01-29 03:30:48 +00:00
if print_status:
self.print_status()
2018-01-08 00:51:19 +00:00
2018-01-09 01:10:09 +00:00
print('\r\n * subcores stopping')
for subcore in self.clients:
2018-08-30 22:19:44 +00:00
subcore.cmd_q.put('x')
2018-01-09 01:10:09 +00:00
for n in range(0, 40): # 2sec
2018-01-09 01:10:09 +00:00
clean_shutdown = True
for subcore in self.clients:
2018-08-30 22:19:44 +00:00
if not subcore.cmd_q.empty():
2018-01-09 01:10:09 +00:00
clean_shutdown = False
break
if clean_shutdown:
print(' * actor stopped')
break
time.sleep(0.05)
if not clean_shutdown:
2018-01-09 01:10:09 +00:00
print(' -X- some subcores are stuck')
for subcore in self.clients:
if not subcore[1].empty():
subcore[0].terminate()
2018-01-08 00:51:19 +00:00
print(' * test ended')
2018-01-08 00:51:19 +00:00
def shutdown(self):
self.stopping = True
def signal_handler(self, signal, frame):
self.shutdown()
2018-01-09 01:10:09 +00:00
if __name__ == '__main__':
core = Core()
core.run()
# cat log | grep -E ' adding msg ' | awk '{printf "%.3f\n", $1-v; v=$1}' | sed -r 's/\.//;s/^0*//;s/^$/0/' | awk 'BEGIN {sum=0} $1<10000 {sum=sum+$1} NR%10==0 {v=sum/32; sum=0; printf "%" v "s\n", "" }' | tr ' ' '#'