Skip to content

Commit 88850bb

Browse files
authored
Merge pull request #46 from psav/psav/node_setup
Node utils and perf testing
2 parents c5cf514 + 09027c0 commit 88850bb

8 files changed

Lines changed: 564 additions & 0 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "Receptor Perf Testing",
3+
"description": "Performance testing of Receptor.",
4+
"iconName": "python",
5+
"categories": ["Python"]
6+
}

.github/workflows/tester.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: Receptor Perf Testing
2+
3+
on: [push, pull_request]
4+
5+
jobs:
6+
build:
7+
8+
runs-on: ubuntu-latest
9+
10+
steps:
11+
- uses: actions/checkout@v1
12+
- name: Set up Python 3.7
13+
uses: actions/setup-python@v1
14+
with:
15+
python-version: 3.7
16+
- name: Install dependencies
17+
run: |
18+
python -m pip install --upgrade pip
19+
pip install python-dateutil
20+
pip install .
21+
pip install -r ./test/perf/requirements.txt
22+
- name: Perform perf test 1 - Random
23+
run: |
24+
python ./test/perf/node_utils.py file ./test/perf/topology-random.yaml&
25+
sleep 10
26+
python ./test/perf/node_utils.py ping ./test/perf/topology-random.yaml --count 100 --validate 0.1
27+
kill `pidof python`
28+
rm -Rf /tmp/receptor
29+
- name: Perform perf test 2 - Flat
30+
run: |
31+
python ./test/perf/node_utils.py file ./test/perf/topology-flat.yaml&
32+
sleep 10
33+
python ./test/perf/node_utils.py ping ./test/perf/topology-flat.yaml --count 100 --validate 0.1
34+
kill `pidof python`
35+
rm -Rf /tmp/receptor
36+
- name: Perform perf test 3 - Tree
37+
run: |
38+
python ./test/perf/node_utils.py file ./test/perf/topology-tree.yaml&
39+
sleep 10
40+
python ./test/perf/node_utils.py ping ./test/perf/topology-tree.yaml --count 100 --validate 0.1
41+
kill `pidof python`
42+
rm -Rf /tmp/receptor

test/perf/__init__.py

Whitespace-only changes.

test/perf/node_utils.py

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
import random
2+
import socket
3+
import subprocess
4+
import sys
5+
import time
6+
from collections import defaultdict
7+
from collections import namedtuple
8+
from time import sleep
9+
10+
import click
11+
import yaml
12+
13+
14+
DEBUG = False
15+
16+
17+
def random_port(tcp=True):
18+
"""Get a random port number for making a socket
19+
20+
Args:
21+
tcp: Return a TCP port number if True, UDP if False
22+
23+
This may not be reliable at all due to an inherent race condition. This works
24+
by creating a socket on an ephemeral port, inspecting it to see what port was used,
25+
closing it, and returning that port number. In the time between closing the socket
26+
and opening a new one, it's possible for the OS to reopen that port for another purpose.
27+
28+
In practical testing, this race condition did not result in a failure to (re)open the
29+
returned port number, making this solution squarely "good enough for now".
30+
"""
31+
# Port 0 will allocate an ephemeral port
32+
socktype = socket.SOCK_STREAM if tcp else socket.SOCK_DGRAM
33+
s = socket.socket(socket.AF_INET, socktype)
34+
s.bind(("", 0))
35+
addr, port = s.getsockname()
36+
s.close()
37+
return port
38+
39+
40+
procs = []
41+
42+
43+
Node = namedtuple("Node", ["name", "controller", "listen_port", "connections"])
44+
45+
46+
def generate_random_mesh(controller_port, node_count, conn_method):
47+
a = {"controller": Node("controller", True, controller_port, [])}
48+
49+
for i in range(node_count):
50+
a[f"node{i}"] = Node(f"node{i}", False, random_port(), [])
51+
52+
for k, node in a.items():
53+
if node.controller:
54+
continue
55+
else:
56+
node.connections.extend(conn_method(a, node))
57+
return a
58+
59+
60+
def do_it(topology, profile=False):
61+
with open("last-topology.yaml", "w") as f:
62+
data = {"nodes": {}}
63+
for node, node_data in topology.items():
64+
data["nodes"][node] = {
65+
"name": node_data.name,
66+
"listen_port": node_data.listen_port if node_data.controller else None,
67+
"controller": node_data.controller,
68+
"connections": node_data.connections,
69+
}
70+
yaml.dump(data, f)
71+
with open("last-topology_graph.dot", "w") as f:
72+
f.write("graph {")
73+
for node, node_data in topology.items():
74+
for conn in node_data.connections:
75+
f.write(f"{node} -- {conn}; ")
76+
f.write("}")
77+
78+
with open("command-log.log", "w") as f:
79+
for k, node in topology.items():
80+
81+
if profile:
82+
start = [
83+
"python",
84+
"-m",
85+
"cProfile",
86+
"-o",
87+
f"{node.name}.prof",
88+
"-m",
89+
"receptor.__main__",
90+
]
91+
else:
92+
start = ["receptor"]
93+
94+
if node.controller:
95+
if not DEBUG:
96+
starter = start[:]
97+
starter.extend(
98+
[
99+
"--debug",
100+
"-d",
101+
"/tmp/receptor",
102+
"--node-id",
103+
"controller",
104+
"controller",
105+
"--socket-path=/tmp/receptor/receptor.sock",
106+
f"--listen-port={node.listen_port}",
107+
]
108+
)
109+
op = subprocess.Popen(" ".join(starter), shell=True)
110+
procs.append(op)
111+
f.write(f"{' '.join(starter)}\n")
112+
sleep(2)
113+
else:
114+
peer_string = " ".join(
115+
[
116+
f"--peer=localhost:{topology[pnode].listen_port}"
117+
for pnode in node.connections
118+
]
119+
)
120+
if not DEBUG:
121+
starter = start[:]
122+
starter.extend(
123+
[
124+
"-d",
125+
"/tmp/receptor",
126+
"--node-id",
127+
node.name,
128+
"node",
129+
f"--listen-port={node.listen_port}",
130+
peer_string,
131+
]
132+
)
133+
op = subprocess.Popen(" ".join(starter), shell=True)
134+
procs.append(op)
135+
f.write(f"{' '.join(starter)}\n")
136+
sleep(0.1)
137+
138+
try:
139+
while True:
140+
sleep(1)
141+
except KeyboardInterrupt:
142+
for proc in procs:
143+
proc.kill()
144+
145+
146+
def load_topology(filename):
147+
data = yaml.safe_load(filename)
148+
topology = {}
149+
for node, definition in data["nodes"].items():
150+
topology[node] = Node(
151+
definition["name"],
152+
definition["controller"],
153+
definition.get("listen_port", None) or random_port(),
154+
definition["connections"],
155+
)
156+
return topology
157+
158+
159+
@click.group(help="Helper commands for application")
160+
def main():
161+
pass
162+
163+
164+
@main.command("random")
165+
@click.option("--debug", is_flag=True, default=False)
166+
@click.option("--controller-port", help="Chooses Controller port", default=8888)
167+
@click.option("--node-count", help="Choose number of nodes", default=10)
168+
@click.option("--max-conn-count", help="Choose max number of connections per node", default=2)
169+
@click.option("--profile", is_flag=True, default=False)
170+
def randomize(controller_port, node_count, max_conn_count, debug):
171+
if debug:
172+
global DEBUG
173+
DEBUG = True
174+
175+
def peer_function(nodes, cur_node):
176+
nconns = defaultdict(int)
177+
print(nodes)
178+
for k, node in nodes.items():
179+
for conn in node.connections:
180+
nconns[conn] += 1
181+
available_nodes = list(filter(lambda o: nconns[o] < max_conn_count, nodes))
182+
print("------")
183+
print(nconns)
184+
print(available_nodes)
185+
print(cur_node.name)
186+
print(random.choices(available_nodes, k=int(random.random() * max_conn_count)))
187+
print("----")
188+
if cur_node.name not in available_nodes:
189+
return []
190+
else:
191+
return random.choices(available_nodes, k=int(random.random() * max_conn_count))
192+
193+
node_topology = generate_random_mesh(controller_port, node_count, peer_function)
194+
print(node_topology)
195+
do_it(node_topology)
196+
197+
198+
@main.command("flat")
199+
@click.option("--debug", is_flag=True, default=False)
200+
@click.option("--controller-port", help="Chooses Controller port", default=8888)
201+
@click.option("--node-count", help="Choose number of nodes", default=10)
202+
@click.option("--profile", is_flag=True, default=False)
203+
def flat(controller_port, node_count, debug):
204+
if debug:
205+
global DEBUG
206+
DEBUG = True
207+
208+
def peer_function(nodes, cur_node):
209+
return ["controller"]
210+
211+
node_topology = generate_random_mesh(controller_port, node_count, peer_function)
212+
print(node_topology)
213+
do_it(node_topology)
214+
215+
216+
@main.command("file")
217+
@click.option("--debug", is_flag=True, default=False)
218+
@click.option("--profile", is_flag=True, default=False)
219+
@click.argument("filename", type=click.File("r"))
220+
def file(filename, debug, profile):
221+
topology = load_topology(filename)
222+
do_it(topology, profile=profile)
223+
224+
225+
@main.command("ping")
226+
@click.option("--validate", default=None)
227+
@click.option("--count", default=10)
228+
@click.argument("filename", type=click.File("r"))
229+
def ping(filename, count, validate):
230+
topology = load_topology(filename)
231+
results = {}
232+
for name, node in topology.items():
233+
starter = [
234+
"time",
235+
"receptor",
236+
"ping",
237+
"--socket-path",
238+
"/tmp/receptor/receptor.sock",
239+
node.name,
240+
"--count",
241+
str(count),
242+
]
243+
start = time.time()
244+
op = subprocess.Popen(" ".join(starter), shell=True, stdout=subprocess.PIPE)
245+
op.wait()
246+
duration = time.time() - start
247+
cmd_output = op.stdout.readlines()
248+
print(cmd_output)
249+
if b"Failed" in cmd_output[0]:
250+
results[node.name] = "Failed"
251+
else:
252+
results[node.name] = duration / count
253+
with open("results.yaml", "w") as f:
254+
yaml.dump(results, f)
255+
if validate:
256+
valid = True
257+
for node in results:
258+
if topology[node].controller:
259+
continue
260+
print(f"Asserting node {node} was under {validate} threshold")
261+
print(f" {results[node]}")
262+
if results[node] == "Failed" or float(results[node]) > float(validate):
263+
valid = False
264+
print(" FAILED!")
265+
else:
266+
print(" PASSED!")
267+
if not valid:
268+
sys.exit(127)
269+
270+
271+
if __name__ == "__main__":
272+
main()

test/perf/requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
click
2+
pyyaml

0 commit comments

Comments
 (0)