2 from random import randrange
11 __author__ = "Jan Medved"
12 __copyright__ = "Copyright(c) 2014, Cisco Systems, Inc."
13 __license__ = "New-style BSD"
14 __email__ = "jmedved@cisco.com"
17 class Counter(object):
18 def __init__(self, start=0):
19 self.lock = threading.Lock()
22 def increment(self, value=1):
33 def __init__(self, verbose=False):
34 self.verbose = verbose
37 self.start = time.time()
40 def __exit__(self, *args):
41 self.end = time.time()
42 self.secs = self.end - self.start
43 self.msecs = self.secs * 1000 # millisecs
45 print("elapsed time: %f ms" % self.msecs)
48 class ShardPerformanceTester(object):
50 The ShardPerformanceTester class facilitates performance testing of CDS shards. The test starts a number of
51 threads, where each thread issues a specified number of resource retrieval requests to URLs specified at the
52 beginning of a test. A ShardPerformanceTester object gets its connection parameters to the system-under-test
53 and its test parameters when it is instantiated. The set of URLs from where to retrieve resources is
54 specified when a test is started. By passing in the appropriate URLs, the test can be used to test data
55 retrieval performance of different shards or different resources at different granularities, etc.
58 headers = {"Accept": "application/json"}
60 def __init__(self, host, port, auth, threads, nrequests, plevel):
65 self.requests = nrequests
66 self.threads = threads
69 self.print_lock = threading.Lock()
70 self.cond = threading.Condition()
74 self.url_counters = []
77 def make_request(self, session, urls):
79 Makes a request for a resource at a random URL selected from a list of URLs passed as input parameter
80 :param session: Session to system under test
81 :param urls: List of resource URLs
82 :return: Status code from the resource request call
84 url_index = randrange(0, len(urls))
85 r_url = urls[url_index]
86 self.url_counters[url_index].increment()
89 r = session.get(r_url, headers=self.headers, stream=False)
92 r_url, headers=self.headers, stream=False, auth=("admin", "admin")
96 def worker(self, tid, urls):
98 Worker thread function. Connects to system-under-test and makes 'self.requests' requests for
99 resources to URLs randomly selected from 'urls'
100 :param tid: Worker thread ID
101 :param urls: List of resource URLs
106 s = requests.Session()
108 with self.print_lock:
109 print(" Thread %d: Performing %d requests" % (tid, self.requests))
112 for r in range(self.requests):
113 sts = self.make_request(s, urls)
119 ok_rate = res[200] / t.secs
120 total_rate = sum(res.values()) / t.secs
122 with self.print_lock:
123 print("Thread %d done:" % tid)
124 print(" Time: %.2f," % t.secs)
125 print(" Success rate: %.2f, Total rate: %.2f" % (ok_rate, total_rate))
126 print(" Per-thread stats: ")
128 self.threads_done += 1
129 self.total_rate += total_rate
134 self.cond.notifyAll()
136 def run_test(self, urls):
138 Runs the performance test. Starts 'self.threads' worker threads, waits for all of them to finish and
140 :param urls: List of urls from which to request resources
147 # Initialize url counters
148 del self.url_counters[:]
149 for i in range(len(urls)):
150 self.url_counters.append(Counter(0))
152 # Start all worker threads
153 for i in range(self.threads):
154 t = threading.Thread(target=self.worker, args=(i, urls))
158 # Wait for all threads to finish and measure the execution time
160 while self.threads_done < self.threads:
164 # Print summary results. Each worker prints its owns results too.
165 print("\nSummary Results:")
167 " Requests/sec (total_sum): %.2f"
168 % ((self.threads * self.requests) / t.secs)
171 " Requests/sec (measured): %.2f"
172 % ((self.threads * self.requests) / t.secs)
174 print(" Time: %.2f" % t.secs)
175 self.threads_done = 0
178 print(" Per URL Counts: ")
179 for i in range(len(urls)):
180 print("%d" % self.url_counters[i].value)
184 class TestUrlGenerator(object):
186 Base abstract class to generate test URLs for ShardPerformanceTester. First, an entire subtree representing
187 a shard or a set of resources is retrieved, then a set of URLS to access small data stanzas is created. This
188 class only defines the framework, the methods that create URL sets are defined in derived classes.
191 def __init__(self, host, port, auth):
194 :param host: Controller's IP address
195 :param port: Controller's RESTCONF port
196 :param auth: Indicates whether to use authentication with default user/password (admin/admin)
202 self.resource_string = ""
204 def url_generator(self, data):
206 Abstract URL generator. Must be overridden in a derived class
207 :param data: Bulk resource data (JSON) from which to generate the URLs
208 :return: List of generated Resources
211 "Abstract class '%s' should never be used standalone"
212 % (self.__class__.__name__)
218 Drives the generation of test URLs. First, it gets a 'bulk' resource (e.g. the entire inventory
219 or the entire topology) from the controller specified during int() and then invokes a resource-specific
220 URL generator to create a set of resource-specific URLs.
222 t_url = "http://" + self.host + ":" + self.port + "/" + self.resource_string
223 headers = {"Accept": "application/json"}
227 r = requests.get(t_url, headers=headers, stream=False)
230 t_url, headers=headers, stream=False, auth=("admin", "admin")
233 if r.status_code != 200:
235 "Failed to get HTTP response from '%s', code %d"
236 % ((t_url, r.status_code))
240 r_url = self.url_generator(json.loads(r.content))
243 "Failed to get json from '%s'. Please make sure you are connected to mininet."
250 class TopoUrlGenerator(TestUrlGenerator):
252 Class to generate test URLs from the topology shard.
253 :return: List of generated Resources
256 def __init__(self, host, port, auth):
257 TestUrlGenerator.__init__(self, host, port, auth)
258 self.resource_string = (
259 "restconf/operational/network-topology:network-topology/topology/flow:1"
262 def url_generator(self, topo_data):
265 nodes = topo_data["topology"][0]["node"]
267 tpoints = node["termination-point"]
268 for tpoint in tpoints:
274 + "/restconf/operational/network-topology:network-topology/topology/flow:1/node/"
276 + "/termination-point/"
279 url_list.append(t_url)
282 print("Error parsing topology json")
286 class InvUrlGenerator(TestUrlGenerator):
288 Class to generate test URLs from the inventory shard.
291 def __init__(self, host, port, auth):
292 TestUrlGenerator.__init__(self, host, port, auth)
293 self.resource_string = "restconf/operational/opendaylight-inventory:nodes"
295 def url_generator(self, inv_data):
298 nodes = inv_data["nodes"]["node"]
300 nconns = node["node-connector"]
307 + "/restconf/operational/opendaylight-inventory:nodes/node/"
311 + "/opendaylight-port-statistics:flow-capable-node-connector-statistics"
313 url_list.append(i_url)
316 print("Error parsing inventory json")
320 if __name__ == "__main__":
321 parser = argparse.ArgumentParser(
322 description="Flow programming performance test: First adds and then deletes flows "
323 "into the config tree, as specified by optional parameters."
329 help="Host where odl controller is running (default is 127.0.0.1)",
334 help="Port on which odl's RESTCONF is listening (default is 8181)",
341 help="Use the ODL default username/password 'admin'/'admin' to authenticate access to REST; "
342 "default: no authentication",
348 help="Number of request worker threads to start in each cycle; default=1. ",
354 help="Number of requests each worker thread will send to the controller; default=100.",
358 choices=["inv", "topo", "topo+inv", "all"],
360 help="Which resource to test: inventory, topology, or both; default both",
366 help="Print level: controls output verbosity. 0-lowest, 1-highest; default 0",
368 in_args = parser.parse_args()
373 # If required, get topology resource URLs
374 if in_args.resource != "inventory":
375 tg = TopoUrlGenerator(in_args.host, in_args.port, in_args.auth)
376 topo_urls += tg.generate()
377 if len(topo_urls) == 0:
378 print("Failed to generate topology URLs")
381 # If required, get inventory resource URLs
382 if in_args.resource != "topology":
383 ig = InvUrlGenerator(in_args.host, in_args.port, in_args.auth)
384 inv_urls += ig.generate()
385 if len(inv_urls) == 0:
386 print("Failed to generate inventory URLs")
389 if in_args.resource == "topo+inv" or in_args.resource == "all":
390 # To have balanced test results, the number of URLs for topology and inventory must be the same
391 if len(topo_urls) != len(inv_urls):
392 print("The number of topology and inventory URLs don't match")
395 st = ShardPerformanceTester(
404 if in_args.resource == "all" or in_args.resource == "topo":
405 print("===================================")
406 print("Testing topology shard performance:")
407 print("===================================")
408 st.run_test(topo_urls)
410 if in_args.resource == "all" or in_args.resource == "inv":
411 print("====================================")
412 print("Testing inventory shard performance:")
413 print("====================================")
414 st.run_test(inv_urls)
416 if in_args.resource == "topo+inv" or in_args.resource == "all":
417 print("===============================================")
418 print("Testing combined shards (topo+inv) performance:")
419 print("===============================================")
420 st.run_test(topo_urls + inv_urls)